jui-tools-mcp-server 2.13.0 → 2.14.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.
- package/data/attribute_definitions.json +124 -74
- package/data/attribute_semantics.json +18 -1
- package/data/component_metadata.json +36 -7
- package/data/type_synonyms.json +167 -0
- package/dist/cli_runner.js +7 -1
- package/dist/spec_loader.d.ts +25 -0
- package/dist/spec_loader.js +71 -0
- package/dist/tools/jui/jui_generate_converter.js +10 -2
- package/dist/tools/jui/jui_generate_project.js +11 -2
- package/dist/tools/spec/get_data_source.js +5 -2
- package/package.json +1 -1
- package/scripts/fetch-definitions.js +1 -1
- package/scripts/fetch-manifest.js +6 -0
- package/src/cli_runner.ts +7 -1
- package/src/spec_loader.ts +85 -0
- package/src/tools/jui/jui_generate_converter.ts +10 -2
- package/src/tools/jui/jui_generate_project.ts +13 -2
- package/src/tools/spec/get_data_source.ts +5 -2
- package/tests/cli_runner.test.ts +38 -1
- package/tests/snapshot_completeness.test.ts +1 -0
- package/tests/staleness_is_content_not_mtime.test.ts +1 -1
- package/tests/tools_cli.test.ts +43 -0
- package/tests/tools_test.test.ts +4 -4
- package/tests/type_synonyms.test.ts +178 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"_description": "Per-component presentation metadata consumed by jsonui-mcp-server. Augments attribute_definitions.json (which defines the raw attribute schema, including binding_direction per attribute) with human descriptions, aliases, platform availability, platform-specific implementation notes, and agent-facing rules. Edit this file to change what `lookup_component` / `search_components` / `lookup_attribute` return from the MCP.",
|
|
3
3
|
"_schema": {
|
|
4
4
|
"description": "string",
|
|
5
|
+
"interactive": "boolean — REQUIRED on every type: whether a user can operate this component on its own (a control, an input, or a scrolling / embedding container). A tappable that holds one is not flattened into a single button for screen readers (shared/core/tap_accessibility.rb). Declared, never derived: Button, Radio and Web declare no two-way binding and are operable.",
|
|
5
6
|
"aliases": "string[]",
|
|
6
7
|
"platforms": {
|
|
7
8
|
"swift_generated": "boolean",
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
},
|
|
16
17
|
"Label": {
|
|
17
18
|
"description": "Text display component",
|
|
19
|
+
"interactive": false,
|
|
18
20
|
"aliases": [
|
|
19
21
|
"Text"
|
|
20
22
|
],
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
},
|
|
50
52
|
"TextField": {
|
|
51
53
|
"description": "Text input field",
|
|
54
|
+
"interactive": true,
|
|
52
55
|
"aliases": [
|
|
53
56
|
"EditText",
|
|
54
57
|
"Input"
|
|
@@ -86,6 +89,7 @@
|
|
|
86
89
|
},
|
|
87
90
|
"Button": {
|
|
88
91
|
"description": "Tappable button component",
|
|
92
|
+
"interactive": true,
|
|
89
93
|
"aliases": [],
|
|
90
94
|
"platforms": {
|
|
91
95
|
"swift_generated": true,
|
|
@@ -117,6 +121,7 @@
|
|
|
117
121
|
},
|
|
118
122
|
"Image": {
|
|
119
123
|
"description": "Local image component",
|
|
124
|
+
"interactive": false,
|
|
120
125
|
"aliases": [
|
|
121
126
|
"CircleImage",
|
|
122
127
|
"CircleImageView",
|
|
@@ -153,6 +158,7 @@
|
|
|
153
158
|
},
|
|
154
159
|
"NetworkImage": {
|
|
155
160
|
"description": "Remote/network image component",
|
|
161
|
+
"interactive": false,
|
|
156
162
|
"aliases": [],
|
|
157
163
|
"platforms": {
|
|
158
164
|
"swift_generated": true,
|
|
@@ -174,12 +180,12 @@
|
|
|
174
180
|
}
|
|
175
181
|
},
|
|
176
182
|
"rules": [
|
|
177
|
-
"Default contentMode is .center for matchParent (Swift)",
|
|
178
183
|
"Supports placeholder/error images via defaultImage"
|
|
179
184
|
]
|
|
180
185
|
},
|
|
181
186
|
"View": {
|
|
182
187
|
"description": "Container view (HStack/VStack/ZStack)",
|
|
188
|
+
"interactive": false,
|
|
183
189
|
"aliases": [],
|
|
184
190
|
"platforms": {
|
|
185
191
|
"swift_generated": true,
|
|
@@ -212,6 +218,7 @@
|
|
|
212
218
|
},
|
|
213
219
|
"ScrollView": {
|
|
214
220
|
"description": "Scrollable container",
|
|
221
|
+
"interactive": true,
|
|
215
222
|
"aliases": [],
|
|
216
223
|
"platforms": {
|
|
217
224
|
"swift_generated": true,
|
|
@@ -238,6 +245,7 @@
|
|
|
238
245
|
},
|
|
239
246
|
"Collection": {
|
|
240
247
|
"description": "List/grid collection with data binding",
|
|
248
|
+
"interactive": true,
|
|
241
249
|
"aliases": [
|
|
242
250
|
"Table",
|
|
243
251
|
"TableView",
|
|
@@ -270,11 +278,12 @@
|
|
|
270
278
|
"Sections support different cell types per section",
|
|
271
279
|
"cellIdProperty extracts unique keys from item data",
|
|
272
280
|
"scrollTo binding enables programmatic scrolling",
|
|
273
|
-
"lazy (default
|
|
281
|
+
"lazy (default 'lazy') is the outer container: 'lazy' virtualizes (LazyVStack/LazyColumn/LazyVerticalGrid with internal scroll); 'eager' draws every cell in a scroll container, with no virtualization; 'none' draws them with NO scroll container (use when nested in an already-scrollable parent). Sticky headers and paging require 'lazy'."
|
|
274
282
|
]
|
|
275
283
|
},
|
|
276
284
|
"Switch": {
|
|
277
285
|
"description": "Toggle switch (iOS style)",
|
|
286
|
+
"interactive": true,
|
|
278
287
|
"aliases": [],
|
|
279
288
|
"platforms": {
|
|
280
289
|
"swift_generated": true,
|
|
@@ -302,6 +311,7 @@
|
|
|
302
311
|
},
|
|
303
312
|
"Toggle": {
|
|
304
313
|
"description": "Toggle control (checkbox style)",
|
|
314
|
+
"interactive": true,
|
|
305
315
|
"aliases": [],
|
|
306
316
|
"platforms": {
|
|
307
317
|
"swift_generated": true,
|
|
@@ -328,6 +338,7 @@
|
|
|
328
338
|
},
|
|
329
339
|
"CheckBox": {
|
|
330
340
|
"description": "Checkbox input",
|
|
341
|
+
"interactive": true,
|
|
331
342
|
"aliases": [
|
|
332
343
|
"Check",
|
|
333
344
|
"Checkbox"
|
|
@@ -357,6 +368,7 @@
|
|
|
357
368
|
},
|
|
358
369
|
"Radio": {
|
|
359
370
|
"description": "Radio button group",
|
|
371
|
+
"interactive": true,
|
|
360
372
|
"aliases": [],
|
|
361
373
|
"platforms": {
|
|
362
374
|
"swift_generated": true,
|
|
@@ -382,6 +394,7 @@
|
|
|
382
394
|
},
|
|
383
395
|
"SelectBox": {
|
|
384
396
|
"description": "Dropdown/picker selection",
|
|
397
|
+
"interactive": true,
|
|
385
398
|
"aliases": [],
|
|
386
399
|
"platforms": {
|
|
387
400
|
"swift_generated": true,
|
|
@@ -407,6 +420,7 @@
|
|
|
407
420
|
},
|
|
408
421
|
"Segment": {
|
|
409
422
|
"description": "Segmented control",
|
|
423
|
+
"interactive": true,
|
|
410
424
|
"aliases": [],
|
|
411
425
|
"platforms": {
|
|
412
426
|
"swift_generated": true,
|
|
@@ -433,6 +447,7 @@
|
|
|
433
447
|
},
|
|
434
448
|
"Slider": {
|
|
435
449
|
"description": "Slider/range input",
|
|
450
|
+
"interactive": true,
|
|
436
451
|
"aliases": [],
|
|
437
452
|
"platforms": {
|
|
438
453
|
"swift_generated": true,
|
|
@@ -454,12 +469,13 @@
|
|
|
454
469
|
},
|
|
455
470
|
"rules": [
|
|
456
471
|
"value/bind is two-way binding",
|
|
457
|
-
"
|
|
472
|
+
"minimum / maximum define the range (default 0 and 1: with neither set the range is 0 .. 1); minimumValue / minValue and maximumValue / maxValue are alias spellings",
|
|
458
473
|
"step defines discrete steps"
|
|
459
474
|
]
|
|
460
475
|
},
|
|
461
476
|
"Progress": {
|
|
462
477
|
"description": "Progress bar/indicator",
|
|
478
|
+
"interactive": false,
|
|
463
479
|
"aliases": [],
|
|
464
480
|
"platforms": {
|
|
465
481
|
"swift_generated": true,
|
|
@@ -485,6 +501,7 @@
|
|
|
485
501
|
},
|
|
486
502
|
"Indicator": {
|
|
487
503
|
"description": "Activity indicator / loading spinner",
|
|
504
|
+
"interactive": false,
|
|
488
505
|
"aliases": [],
|
|
489
506
|
"platforms": {
|
|
490
507
|
"swift_generated": true,
|
|
@@ -508,11 +525,12 @@
|
|
|
508
525
|
},
|
|
509
526
|
"CircleView": {
|
|
510
527
|
"description": "Circular shape view",
|
|
528
|
+
"interactive": false,
|
|
511
529
|
"aliases": [],
|
|
512
530
|
"platforms": {
|
|
513
531
|
"swift_generated": false,
|
|
514
532
|
"swift_dynamic": false,
|
|
515
|
-
"kotlin_generated":
|
|
533
|
+
"kotlin_generated": true,
|
|
516
534
|
"kotlin_dynamic": true,
|
|
517
535
|
"react": true
|
|
518
536
|
},
|
|
@@ -531,6 +549,7 @@
|
|
|
531
549
|
},
|
|
532
550
|
"GradientView": {
|
|
533
551
|
"description": "Gradient background view",
|
|
552
|
+
"interactive": false,
|
|
534
553
|
"aliases": [],
|
|
535
554
|
"platforms": {
|
|
536
555
|
"swift_generated": true,
|
|
@@ -556,6 +575,7 @@
|
|
|
556
575
|
},
|
|
557
576
|
"Blur": {
|
|
558
577
|
"description": "Blur effect view",
|
|
578
|
+
"interactive": false,
|
|
559
579
|
"aliases": [],
|
|
560
580
|
"platforms": {
|
|
561
581
|
"swift_generated": true,
|
|
@@ -581,6 +601,7 @@
|
|
|
581
601
|
},
|
|
582
602
|
"IconLabel": {
|
|
583
603
|
"description": "Icon with label combination",
|
|
604
|
+
"interactive": false,
|
|
584
605
|
"aliases": [],
|
|
585
606
|
"platforms": {
|
|
586
607
|
"swift_generated": true,
|
|
@@ -606,6 +627,7 @@
|
|
|
606
627
|
},
|
|
607
628
|
"Web": {
|
|
608
629
|
"description": "Web view / embedded browser",
|
|
630
|
+
"interactive": true,
|
|
609
631
|
"aliases": [],
|
|
610
632
|
"platforms": {
|
|
611
633
|
"swift_generated": true,
|
|
@@ -626,11 +648,13 @@
|
|
|
626
648
|
}
|
|
627
649
|
},
|
|
628
650
|
"rules": [
|
|
629
|
-
"url attribute specifies the web page to load"
|
|
651
|
+
"url attribute specifies the web page to load",
|
|
652
|
+
"Error face + retry (iOS / Android only): bind onLoadFailed to a ViewModel method that hides the Web and shows the error, and bind reloadToken to an Int the retry method increments — the Web reloads its own url each time the value changes. Rewriting url to the same value does not reload."
|
|
630
653
|
]
|
|
631
654
|
},
|
|
632
655
|
"SafeAreaView": {
|
|
633
656
|
"description": "Safe area respecting container",
|
|
657
|
+
"interactive": false,
|
|
634
658
|
"aliases": [],
|
|
635
659
|
"platforms": {
|
|
636
660
|
"swift_generated": true,
|
|
@@ -653,6 +677,7 @@
|
|
|
653
677
|
},
|
|
654
678
|
"TabView": {
|
|
655
679
|
"description": "Tab-based navigation container",
|
|
680
|
+
"interactive": true,
|
|
656
681
|
"aliases": [],
|
|
657
682
|
"platforms": {
|
|
658
683
|
"swift_generated": true,
|
|
@@ -678,6 +703,7 @@
|
|
|
678
703
|
},
|
|
679
704
|
"TextView": {
|
|
680
705
|
"description": "Multi-line text input",
|
|
706
|
+
"interactive": true,
|
|
681
707
|
"aliases": [],
|
|
682
708
|
"platforms": {
|
|
683
709
|
"swift_generated": true,
|
|
@@ -704,11 +730,12 @@
|
|
|
704
730
|
},
|
|
705
731
|
"EditText": {
|
|
706
732
|
"description": "Text input (Android naming convention)",
|
|
733
|
+
"interactive": true,
|
|
707
734
|
"aliases": [],
|
|
708
735
|
"platforms": {
|
|
709
736
|
"swift_generated": true,
|
|
710
737
|
"swift_dynamic": true,
|
|
711
|
-
"kotlin_generated":
|
|
738
|
+
"kotlin_generated": true,
|
|
712
739
|
"kotlin_dynamic": true,
|
|
713
740
|
"react": true
|
|
714
741
|
},
|
|
@@ -724,11 +751,12 @@
|
|
|
724
751
|
},
|
|
725
752
|
"Input": {
|
|
726
753
|
"description": "Generic input (React naming convention)",
|
|
754
|
+
"interactive": true,
|
|
727
755
|
"aliases": [],
|
|
728
756
|
"platforms": {
|
|
729
757
|
"swift_generated": true,
|
|
730
758
|
"swift_dynamic": true,
|
|
731
|
-
"kotlin_generated":
|
|
759
|
+
"kotlin_generated": true,
|
|
732
760
|
"kotlin_dynamic": true,
|
|
733
761
|
"react": true
|
|
734
762
|
},
|
|
@@ -744,6 +772,7 @@
|
|
|
744
772
|
},
|
|
745
773
|
"Embed": {
|
|
746
774
|
"description": "Embeds another screen as a region of this layout. The embedded screen owns its own ViewModel — independent from the parent VM.",
|
|
775
|
+
"interactive": true,
|
|
747
776
|
"aliases": [],
|
|
748
777
|
"platforms": {
|
|
749
778
|
"swift_generated": true,
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_description": [
|
|
3
|
+
"Type-spelling synonyms: a node `type` that is not a section of attribute_definitions.json, mapped to the section that declares it (`canonical`).",
|
|
4
|
+
"The one table. Validation and the renderers are to read it, and hold no list of their own; which of them do is measured by their own tests (the type_synonyms_draw specs, SwiftJsonUI's TypeSynonymsDrawTests), not recorded here. It does not apply to sjui's UIKit mode, a separate renderer with its own set of UIKit components and its own spellings for them (Table is its SJUITableView); that mode draws what it has.",
|
|
5
|
+
"A renderer draws a synonym as its canonical type — except where the entry names `render_as`: the spelling draws differently from its canonical type (CircleImage clips to a circle), and the renderer draws `render_as` with the converter of that name, while validation still reads the canonical section. A type an app registers as its own component is the app's, whatever its spelling, and is never rewritten.",
|
|
6
|
+
"Any other key of an entry is an attribute the spelling itself means, of the canonical section: HStack and Row are a View with `orientation: horizontal`, VStack and Column one with `vertical`. A renderer adds it when the node does not set it. When the node sets it otherwise (an HStack with `orientation: vertical`), the node's value is drawn and the renderer warns, naming both. ZStack and Box add nothing: a View without orientation is already the stack they mean.",
|
|
7
|
+
"Not here: EditText / Input / Check / Toggle. They are sections of attribute_definitions.json that point at their canonical section with `_alias_of`; the exact-match lookup finds them first. Spacer / Divider / Triangle are neither declared nor synonyms, and draw as undeclared types."
|
|
8
|
+
],
|
|
9
|
+
"synonyms": {
|
|
10
|
+
"Text": {
|
|
11
|
+
"canonical": "Label"
|
|
12
|
+
},
|
|
13
|
+
"MultiLineEditText": {
|
|
14
|
+
"canonical": "TextView"
|
|
15
|
+
},
|
|
16
|
+
"Textarea": {
|
|
17
|
+
"canonical": "TextView"
|
|
18
|
+
},
|
|
19
|
+
"ImageView": {
|
|
20
|
+
"canonical": "Image"
|
|
21
|
+
},
|
|
22
|
+
"Img": {
|
|
23
|
+
"canonical": "Image"
|
|
24
|
+
},
|
|
25
|
+
"NetworkImageView": {
|
|
26
|
+
"canonical": "NetworkImage"
|
|
27
|
+
},
|
|
28
|
+
"CircleImage": {
|
|
29
|
+
"canonical": "Image",
|
|
30
|
+
"render_as": "CircleImage"
|
|
31
|
+
},
|
|
32
|
+
"CircleImageView": {
|
|
33
|
+
"canonical": "Image",
|
|
34
|
+
"render_as": "CircleImage"
|
|
35
|
+
},
|
|
36
|
+
"AsyncImage": {
|
|
37
|
+
"canonical": "NetworkImage"
|
|
38
|
+
},
|
|
39
|
+
"Spinner": {
|
|
40
|
+
"canonical": "SelectBox"
|
|
41
|
+
},
|
|
42
|
+
"DatePicker": {
|
|
43
|
+
"canonical": "SelectBox"
|
|
44
|
+
},
|
|
45
|
+
"Select": {
|
|
46
|
+
"canonical": "SelectBox"
|
|
47
|
+
},
|
|
48
|
+
"Picker": {
|
|
49
|
+
"canonical": "SelectBox"
|
|
50
|
+
},
|
|
51
|
+
"Checkbox": {
|
|
52
|
+
"canonical": "CheckBox"
|
|
53
|
+
},
|
|
54
|
+
"RadioButton": {
|
|
55
|
+
"canonical": "Radio"
|
|
56
|
+
},
|
|
57
|
+
"RadioGroup": {
|
|
58
|
+
"canonical": "Radio"
|
|
59
|
+
},
|
|
60
|
+
"SegmentedControl": {
|
|
61
|
+
"canonical": "Segment"
|
|
62
|
+
},
|
|
63
|
+
"TabLayout": {
|
|
64
|
+
"canonical": "Segment"
|
|
65
|
+
},
|
|
66
|
+
"TabGroup": {
|
|
67
|
+
"canonical": "Segment"
|
|
68
|
+
},
|
|
69
|
+
"SeekBar": {
|
|
70
|
+
"canonical": "Slider"
|
|
71
|
+
},
|
|
72
|
+
"Range": {
|
|
73
|
+
"canonical": "Slider"
|
|
74
|
+
},
|
|
75
|
+
"ProgressBar": {
|
|
76
|
+
"canonical": "Progress"
|
|
77
|
+
},
|
|
78
|
+
"ActivityIndicator": {
|
|
79
|
+
"canonical": "Indicator"
|
|
80
|
+
},
|
|
81
|
+
"Loading": {
|
|
82
|
+
"canonical": "Indicator"
|
|
83
|
+
},
|
|
84
|
+
"LinearLayout": {
|
|
85
|
+
"canonical": "View"
|
|
86
|
+
},
|
|
87
|
+
"RelativeLayout": {
|
|
88
|
+
"canonical": "View"
|
|
89
|
+
},
|
|
90
|
+
"FrameLayout": {
|
|
91
|
+
"canonical": "View"
|
|
92
|
+
},
|
|
93
|
+
"HStack": {
|
|
94
|
+
"canonical": "View",
|
|
95
|
+
"orientation": "horizontal"
|
|
96
|
+
},
|
|
97
|
+
"VStack": {
|
|
98
|
+
"canonical": "View",
|
|
99
|
+
"orientation": "vertical"
|
|
100
|
+
},
|
|
101
|
+
"ZStack": {
|
|
102
|
+
"canonical": "View"
|
|
103
|
+
},
|
|
104
|
+
"Div": {
|
|
105
|
+
"canonical": "View"
|
|
106
|
+
},
|
|
107
|
+
"Box": {
|
|
108
|
+
"canonical": "View"
|
|
109
|
+
},
|
|
110
|
+
"Container": {
|
|
111
|
+
"canonical": "View"
|
|
112
|
+
},
|
|
113
|
+
"Column": {
|
|
114
|
+
"canonical": "View",
|
|
115
|
+
"orientation": "vertical"
|
|
116
|
+
},
|
|
117
|
+
"Row": {
|
|
118
|
+
"canonical": "View",
|
|
119
|
+
"orientation": "horizontal"
|
|
120
|
+
},
|
|
121
|
+
"ConstraintLayout": {
|
|
122
|
+
"canonical": "View"
|
|
123
|
+
},
|
|
124
|
+
"Scroll": {
|
|
125
|
+
"canonical": "ScrollView"
|
|
126
|
+
},
|
|
127
|
+
"CollectionView": {
|
|
128
|
+
"canonical": "Collection"
|
|
129
|
+
},
|
|
130
|
+
"RecyclerView": {
|
|
131
|
+
"canonical": "Collection"
|
|
132
|
+
},
|
|
133
|
+
"Table": {
|
|
134
|
+
"canonical": "Collection"
|
|
135
|
+
},
|
|
136
|
+
"TableView": {
|
|
137
|
+
"canonical": "Collection"
|
|
138
|
+
},
|
|
139
|
+
"List": {
|
|
140
|
+
"canonical": "Collection"
|
|
141
|
+
},
|
|
142
|
+
"Grid": {
|
|
143
|
+
"canonical": "Collection"
|
|
144
|
+
},
|
|
145
|
+
"LazyGrid": {
|
|
146
|
+
"canonical": "Collection"
|
|
147
|
+
},
|
|
148
|
+
"ListView": {
|
|
149
|
+
"canonical": "Collection"
|
|
150
|
+
},
|
|
151
|
+
"LazyColumn": {
|
|
152
|
+
"canonical": "Collection"
|
|
153
|
+
},
|
|
154
|
+
"Gradient": {
|
|
155
|
+
"canonical": "GradientView"
|
|
156
|
+
},
|
|
157
|
+
"BlurView": {
|
|
158
|
+
"canonical": "Blur"
|
|
159
|
+
},
|
|
160
|
+
"WebView": {
|
|
161
|
+
"canonical": "Web"
|
|
162
|
+
},
|
|
163
|
+
"Iframe": {
|
|
164
|
+
"canonical": "Web"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
package/dist/cli_runner.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile } from "child_process";
|
|
|
2
2
|
export async function runCli(command, args, options) {
|
|
3
3
|
const timeout = options.timeout ?? 60_000;
|
|
4
4
|
return new Promise((resolve) => {
|
|
5
|
-
execFile(command, args, {
|
|
5
|
+
const child = execFile(command, args, {
|
|
6
6
|
cwd: options.cwd,
|
|
7
7
|
timeout,
|
|
8
8
|
maxBuffer: 10 * 1024 * 1024,
|
|
@@ -22,6 +22,12 @@ export async function runCli(command, args, options) {
|
|
|
22
22
|
stderr: stderr || "",
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
|
+
// Nothing is ever written to the child's stdin: end it now, so a CLI that
|
|
26
|
+
// reads stdin — an overwrite prompt — sees end-of-file instead of waiting
|
|
27
|
+
// for a line that never comes. Through 2.13.1 the pipe stayed open, and such
|
|
28
|
+
// a child waited until the timeout above killed it (`jui g converter` on
|
|
29
|
+
// an existing component, measured 2026-09-26).
|
|
30
|
+
child.stdin?.end();
|
|
25
31
|
});
|
|
26
32
|
}
|
|
27
33
|
export function formatResult(result) {
|
package/dist/spec_loader.d.ts
CHANGED
|
@@ -58,6 +58,13 @@ export interface ReloadRecord {
|
|
|
58
58
|
ok: boolean;
|
|
59
59
|
error?: string;
|
|
60
60
|
}
|
|
61
|
+
export interface TypeSynonym {
|
|
62
|
+
spelling: string;
|
|
63
|
+
canonical: string;
|
|
64
|
+
renderAs?: string;
|
|
65
|
+
implies?: Record<string, unknown>;
|
|
66
|
+
source: string;
|
|
67
|
+
}
|
|
61
68
|
export interface DataSourceInfo {
|
|
62
69
|
attributeDefinitions: FileInfo;
|
|
63
70
|
componentMetadata: FileInfo;
|
|
@@ -66,6 +73,7 @@ export interface DataSourceInfo {
|
|
|
66
73
|
attributeSemantics: FileInfo | null;
|
|
67
74
|
platformSemantics: FileInfo | null;
|
|
68
75
|
coverage: FileInfo | null;
|
|
76
|
+
typeSynonyms: FileInfo | null;
|
|
69
77
|
componentCount: number;
|
|
70
78
|
commonAttributeCount: number;
|
|
71
79
|
}
|
|
@@ -92,6 +100,7 @@ export declare class SpecLoader {
|
|
|
92
100
|
private screenIdentity;
|
|
93
101
|
private commonAttributes;
|
|
94
102
|
private aliasMap;
|
|
103
|
+
private typeSynonyms;
|
|
95
104
|
private metadata;
|
|
96
105
|
private dataSource;
|
|
97
106
|
/** When this process read the files. Data is cached in memory from then
|
|
@@ -156,6 +165,22 @@ export declare class SpecLoader {
|
|
|
156
165
|
listComponents(): string[];
|
|
157
166
|
getDataSource(): DataSourceInfo;
|
|
158
167
|
private load;
|
|
168
|
+
/**
|
|
169
|
+
* Type spellings the build accepts for a section they are not
|
|
170
|
+
* (shared/core/type_synonyms.json, jsonui-cli 1.9.0 — the one table the
|
|
171
|
+
* tools and the dynamic runtimes read). Until then lookup_component knew
|
|
172
|
+
* only `_alias_of` sections and metadata aliases, and answered "not found"
|
|
173
|
+
* for `ProgressBar`, which the build draws as a Progress.
|
|
174
|
+
*
|
|
175
|
+
* A section, an `_alias_of` section or a metadata alias of the same
|
|
176
|
+
* spelling wins: the exact match is found first, as in the tools. An entry
|
|
177
|
+
* whose canonical is not a section here is skipped.
|
|
178
|
+
*
|
|
179
|
+
* Absent (older jsonui-cli): no synonyms. Present but not JSON: the load
|
|
180
|
+
* throws, so an automatic reload keeps the last content that parsed rather
|
|
181
|
+
* than swapping in a loader that silently lost every synonym.
|
|
182
|
+
*/
|
|
183
|
+
private loadTypeSynonyms;
|
|
159
184
|
private normalizeMetadata;
|
|
160
185
|
private getMetadata;
|
|
161
186
|
private buildComponentSpec;
|
package/dist/spec_loader.js
CHANGED
|
@@ -59,6 +59,10 @@ export class SpecLoader {
|
|
|
59
59
|
screenIdentity = null;
|
|
60
60
|
commonAttributes = null;
|
|
61
61
|
aliasMap = new Map();
|
|
62
|
+
/// Lower-cased spelling -> its entry in type_synonyms.json, for the
|
|
63
|
+
/// spellings that resolve through it (not a section, `_alias_of` or
|
|
64
|
+
/// metadata alias of their own).
|
|
65
|
+
typeSynonyms = new Map();
|
|
62
66
|
metadata = {};
|
|
63
67
|
dataSource;
|
|
64
68
|
/** When this process read the files. Data is cached in memory from then
|
|
@@ -123,6 +127,13 @@ export class SpecLoader {
|
|
|
123
127
|
const comp = this.getComponent(name);
|
|
124
128
|
if (!comp)
|
|
125
129
|
return null;
|
|
130
|
+
// Say which spelling was asked for and why it resolved: the build
|
|
131
|
+
// accepts `ProgressBar` as a Progress, and an HStack is a View that
|
|
132
|
+
// also means `orientation: horizontal`.
|
|
133
|
+
const synonym = this.typeSynonyms.get(name.toLowerCase());
|
|
134
|
+
if (synonym) {
|
|
135
|
+
return { ...comp, typeSynonym: synonym, commonAttributes: this.commonAttributes };
|
|
136
|
+
}
|
|
126
137
|
return { ...comp, commonAttributes: this.commonAttributes };
|
|
127
138
|
}
|
|
128
139
|
getAttribute(name) {
|
|
@@ -202,6 +213,15 @@ export class SpecLoader {
|
|
|
202
213
|
matches.push(`rule: ${rule}`);
|
|
203
214
|
}
|
|
204
215
|
}
|
|
216
|
+
// A spelling the build accepts for this type (type_synonyms.json).
|
|
217
|
+
for (const synonym of this.typeSynonyms.values()) {
|
|
218
|
+
if (this.getComponent(synonym.spelling) !== comp)
|
|
219
|
+
continue;
|
|
220
|
+
if (synonym.spelling.toLowerCase().includes(q)) {
|
|
221
|
+
score += 8;
|
|
222
|
+
matches.push(`type synonym: ${synonym.spelling}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
205
225
|
if (score > 0)
|
|
206
226
|
results.push({ component: comp.name, score, matches });
|
|
207
227
|
}
|
|
@@ -290,6 +310,7 @@ export class SpecLoader {
|
|
|
290
310
|
this.dataSource.attributeSemantics,
|
|
291
311
|
this.dataSource.platformSemantics,
|
|
292
312
|
this.dataSource.coverage,
|
|
313
|
+
this.dataSource.typeSynonyms,
|
|
293
314
|
].filter((f) => f != null);
|
|
294
315
|
const changed = [];
|
|
295
316
|
for (const file of tracked) {
|
|
@@ -458,6 +479,7 @@ export class SpecLoader {
|
|
|
458
479
|
}
|
|
459
480
|
componentCount++;
|
|
460
481
|
}
|
|
482
|
+
const typeSynonymsResolution = this.loadTypeSynonyms();
|
|
461
483
|
this.dataSource = {
|
|
462
484
|
attributeDefinitions: attrResolution,
|
|
463
485
|
componentMetadata: metaResolution,
|
|
@@ -466,10 +488,59 @@ export class SpecLoader {
|
|
|
466
488
|
attributeSemantics: attributeSemanticsResolution,
|
|
467
489
|
platformSemantics: platformSemanticsResolution,
|
|
468
490
|
coverage: coverageResolution,
|
|
491
|
+
typeSynonyms: typeSynonymsResolution,
|
|
469
492
|
componentCount,
|
|
470
493
|
commonAttributeCount: Object.keys(this.commonAttributesRaw).filter((k) => !k.startsWith("_")).length,
|
|
471
494
|
};
|
|
472
495
|
}
|
|
496
|
+
/**
|
|
497
|
+
* Type spellings the build accepts for a section they are not
|
|
498
|
+
* (shared/core/type_synonyms.json, jsonui-cli 1.9.0 — the one table the
|
|
499
|
+
* tools and the dynamic runtimes read). Until then lookup_component knew
|
|
500
|
+
* only `_alias_of` sections and metadata aliases, and answered "not found"
|
|
501
|
+
* for `ProgressBar`, which the build draws as a Progress.
|
|
502
|
+
*
|
|
503
|
+
* A section, an `_alias_of` section or a metadata alias of the same
|
|
504
|
+
* spelling wins: the exact match is found first, as in the tools. An entry
|
|
505
|
+
* whose canonical is not a section here is skipped.
|
|
506
|
+
*
|
|
507
|
+
* Absent (older jsonui-cli): no synonyms. Present but not JSON: the load
|
|
508
|
+
* throws, so an automatic reload keeps the last content that parsed rather
|
|
509
|
+
* than swapping in a loader that silently lost every synonym.
|
|
510
|
+
*/
|
|
511
|
+
loadTypeSynonyms() {
|
|
512
|
+
this.typeSynonyms = new Map();
|
|
513
|
+
let resolution;
|
|
514
|
+
try {
|
|
515
|
+
resolution = this.resolveFile("shared/core/type_synonyms.json", "data/type_synonyms.json");
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
const doc = JSON.parse(readFileSync(resolution.path, "utf-8"));
|
|
521
|
+
for (const [spelling, entry] of Object.entries(doc.synonyms ?? {})) {
|
|
522
|
+
if (typeof entry !== "object" || entry === null)
|
|
523
|
+
continue;
|
|
524
|
+
const { canonical, render_as: renderAs, ...implies } = entry;
|
|
525
|
+
if (typeof canonical !== "string" || canonical.length === 0)
|
|
526
|
+
continue;
|
|
527
|
+
const key = spelling.toLowerCase();
|
|
528
|
+
if (this.components.has(key) || this.aliasMap.has(key))
|
|
529
|
+
continue;
|
|
530
|
+
const target = this.aliasMap.get(canonical.toLowerCase()) ?? canonical.toLowerCase();
|
|
531
|
+
if (!this.components.has(target))
|
|
532
|
+
continue;
|
|
533
|
+
this.aliasMap.set(key, target);
|
|
534
|
+
this.typeSynonyms.set(key, {
|
|
535
|
+
spelling,
|
|
536
|
+
canonical,
|
|
537
|
+
...(typeof renderAs === "string" ? { renderAs } : {}),
|
|
538
|
+
...(Object.keys(implies).length > 0 ? { implies } : {}),
|
|
539
|
+
source: "shared/core/type_synonyms.json",
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return resolution;
|
|
543
|
+
}
|
|
473
544
|
normalizeMetadata(raw) {
|
|
474
545
|
return {
|
|
475
546
|
description: raw.description ?? "",
|
|
@@ -6,7 +6,10 @@ export function register(server, config) {
|
|
|
6
6
|
from_spec: z.string().optional().describe("Generate from component spec file path"),
|
|
7
7
|
all: z.boolean().optional().describe("Generate from all component specs"),
|
|
8
8
|
attributes: z.string().optional().describe("Attributes in key:type format (e.g., 'prop1:String,prop2:Int')"),
|
|
9
|
-
container: z.boolean().optional().describe("
|
|
9
|
+
container: z.boolean().optional().describe("true: a container (it draws its children). false: a leaf (it takes no " +
|
|
10
|
+
"children, and a layout that gives it some is refused by name) — needs " +
|
|
11
|
+
"jsonui-cli 1.9.0 or later (earlier `jui` rejects --no-container). " +
|
|
12
|
+
"Absent: the default form."),
|
|
10
13
|
skip_existing: z.boolean().optional().describe("Leave existing converter and scaffold files untouched, without the " +
|
|
11
14
|
"overwrite prompt — for idempotent scaffold runs. Wins over `force` " +
|
|
12
15
|
"when both are set."),
|
|
@@ -31,9 +34,14 @@ export function register(server, config) {
|
|
|
31
34
|
if (params.attributes) {
|
|
32
35
|
args.push("--attributes", params.attributes);
|
|
33
36
|
}
|
|
34
|
-
|
|
37
|
+
// `false` asks for a leaf. It used to pass nothing, so the command ran
|
|
38
|
+
// in the default form and the request was dropped without a word.
|
|
39
|
+
if (params.container === true) {
|
|
35
40
|
args.push("--container");
|
|
36
41
|
}
|
|
42
|
+
if (params.container === false) {
|
|
43
|
+
args.push("--no-container");
|
|
44
|
+
}
|
|
37
45
|
if (params.skip_existing) {
|
|
38
46
|
args.push("--skip-existing");
|
|
39
47
|
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { runCli, formatResult } from "../../cli_runner.js";
|
|
3
3
|
export function register(server, config) {
|
|
4
|
-
server.tool("jui_generate_project", "Generate Layout JSON and ViewModel files from screen specs (all or single spec)"
|
|
4
|
+
server.tool("jui_generate_project", "Generate Layout JSON and ViewModel files from screen specs (all or single spec). " +
|
|
5
|
+
"A Layout JSON it generates — a screen's, or a Collection cell / header / footer's — is " +
|
|
6
|
+
"written when it is not there, left as it is when it already holds exactly what the spec " +
|
|
7
|
+
"generates, and otherwise KEPT (the Layout JSON is hand-edited after generation) and named " +
|
|
8
|
+
"in the output with \"Kept existing … layout: <file> … --force replaces it\". From " +
|
|
9
|
+
"jsonui-cli 1.9.0; an earlier jui rewrote an existing layout on every run.", {
|
|
5
10
|
spec_file: z.string().optional().describe("Single spec file to process (e.g., 'login.spec.json')"),
|
|
6
|
-
force: z.boolean().optional().describe("
|
|
11
|
+
force: z.boolean().optional().describe("Replace the ViewModel declaration files, and each Layout JSON that already exists and " +
|
|
12
|
+
"differs from what the spec generates (kept and named without it). A layout whose data " +
|
|
13
|
+
"section holds entries the spec does not declare is not replaced even with force — the " +
|
|
14
|
+
"output names them. Called from here the command has no terminal, so it never asks: " +
|
|
15
|
+
"without force an existing layout is kept."),
|
|
7
16
|
skip_layout: z.boolean().optional().describe("Skip Layout JSON generation"),
|
|
8
17
|
dry_run: z.boolean().optional().describe("Show what would be generated without writing"),
|
|
9
18
|
platform: z.enum(["ios", "android", "web"]).optional().describe("Generate for single platform only"),
|