sbuilder-mcp 0.2.2 → 0.3.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/CHANGELOG.md +22 -0
- package/CHANGELOG.vi.md +22 -0
- package/README.md +11 -4
- package/README.vi.md +11 -4
- package/dist/catalog/api.generated.js +1350 -127
- package/dist/catalog/elements.generated.js +345 -2
- package/dist/core/tree.js +9 -0
- package/dist/domains/site/builder.js +5 -9
- package/dist/domains/site/document.js +34 -2
- package/dist/domains/site/node.js +50 -7
- package/dist/domains/site/readiness.js +41 -0
- package/dist/domains/site/review.js +23 -2
- package/dist/domains/site/traps.js +38 -1
- package/dist/install/index.js +40 -0
- package/dist/server.js +7 -1
- package/dist/tools/api.js +15 -1
- package/dist/tools/context.js +18 -1
- package/dist/tools/live.js +169 -15
- package/dist/tools/page.js +29 -17
- package/dist/tools/session.js +35 -10
- package/dist/transport/media.js +26 -5
- package/dist/vision/measure.js +19 -3
- package/dist/vision/shoot.js +39 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const ELEMENT_SOURCE = {
|
|
2
|
-
"count":
|
|
2
|
+
"count": 107,
|
|
3
3
|
"docSchemaVersion": 2
|
|
4
4
|
};
|
|
5
5
|
export const ELEMENTS = {
|
|
@@ -670,6 +670,34 @@ export const ELEMENTS = {
|
|
|
670
670
|
"locked": false,
|
|
671
671
|
"hideInLayer": false,
|
|
672
672
|
"childAllows": [],
|
|
673
|
+
"events": {
|
|
674
|
+
"click": [
|
|
675
|
+
"go_to_url",
|
|
676
|
+
"open_page",
|
|
677
|
+
"popup",
|
|
678
|
+
"open_menu",
|
|
679
|
+
"close_menu",
|
|
680
|
+
"toggle_menu",
|
|
681
|
+
"open_cart",
|
|
682
|
+
"close_cart",
|
|
683
|
+
"go_to_checkout",
|
|
684
|
+
"cart_remove",
|
|
685
|
+
"cart_increment",
|
|
686
|
+
"cart_decrement",
|
|
687
|
+
"change_tab",
|
|
688
|
+
"open_dropdown",
|
|
689
|
+
"close_dropdown",
|
|
690
|
+
"toggle_dropdown",
|
|
691
|
+
"logout_customer",
|
|
692
|
+
"toggle_wishlist"
|
|
693
|
+
]
|
|
694
|
+
},
|
|
695
|
+
"bindingEvents": {
|
|
696
|
+
"click": [
|
|
697
|
+
"open_cart",
|
|
698
|
+
"go_to_checkout"
|
|
699
|
+
]
|
|
700
|
+
},
|
|
673
701
|
"defaults": {
|
|
674
702
|
"specials": {
|
|
675
703
|
"text": "Button",
|
|
@@ -878,6 +906,17 @@ export const ELEMENTS = {
|
|
|
878
906
|
"locked": false,
|
|
879
907
|
"hideInLayer": false,
|
|
880
908
|
"childAllows": [],
|
|
909
|
+
"events": {
|
|
910
|
+
"click": [
|
|
911
|
+
"go_to_url",
|
|
912
|
+
"open_page",
|
|
913
|
+
"popup",
|
|
914
|
+
"change_tab",
|
|
915
|
+
"open_dropdown",
|
|
916
|
+
"close_dropdown",
|
|
917
|
+
"toggle_dropdown"
|
|
918
|
+
]
|
|
919
|
+
},
|
|
881
920
|
"defaults": {
|
|
882
921
|
"specials": {
|
|
883
922
|
"src": "",
|
|
@@ -1025,6 +1064,28 @@ export const ELEMENTS = {
|
|
|
1025
1064
|
"locked": false,
|
|
1026
1065
|
"hideInLayer": false,
|
|
1027
1066
|
"childAllows": [],
|
|
1067
|
+
"events": {
|
|
1068
|
+
"click": [
|
|
1069
|
+
"go_to_url",
|
|
1070
|
+
"open_page",
|
|
1071
|
+
"popup",
|
|
1072
|
+
"open_menu",
|
|
1073
|
+
"close_menu",
|
|
1074
|
+
"toggle_menu",
|
|
1075
|
+
"open_cart",
|
|
1076
|
+
"close_cart",
|
|
1077
|
+
"go_to_checkout",
|
|
1078
|
+
"cart_remove",
|
|
1079
|
+
"cart_increment",
|
|
1080
|
+
"cart_decrement",
|
|
1081
|
+
"change_tab",
|
|
1082
|
+
"open_dropdown",
|
|
1083
|
+
"close_dropdown",
|
|
1084
|
+
"toggle_dropdown",
|
|
1085
|
+
"logout_customer",
|
|
1086
|
+
"toggle_wishlist"
|
|
1087
|
+
]
|
|
1088
|
+
},
|
|
1028
1089
|
"defaults": {
|
|
1029
1090
|
"specials": {
|
|
1030
1091
|
"name": "StarFill",
|
|
@@ -3984,6 +4045,170 @@ export const ELEMENTS = {
|
|
|
3984
4045
|
"signed-in"
|
|
3985
4046
|
]
|
|
3986
4047
|
},
|
|
4048
|
+
"order-receipt": {
|
|
4049
|
+
"type": "order-receipt",
|
|
4050
|
+
"label": "Order receipt",
|
|
4051
|
+
"category": "advanced",
|
|
4052
|
+
"isContainer": false,
|
|
4053
|
+
"isRootOnly": false,
|
|
4054
|
+
"locked": false,
|
|
4055
|
+
"hideInLayer": false,
|
|
4056
|
+
"childAllows": [],
|
|
4057
|
+
"defaults": {
|
|
4058
|
+
"specials": {
|
|
4059
|
+
"title": "Chi tiết đơn hàng",
|
|
4060
|
+
"numberLabel": "Mã đơn hàng",
|
|
4061
|
+
"placedLabel": "Ngày đặt",
|
|
4062
|
+
"statusLabel": "Trạng thái",
|
|
4063
|
+
"itemsLabel": "Sản phẩm",
|
|
4064
|
+
"totalLabel": "Tổng cộng",
|
|
4065
|
+
"dueLabel": "Còn phải trả",
|
|
4066
|
+
"unavailableText": "Không tải được chi tiết đơn hàng. Bạn có thể tra cứu bằng mã đơn hàng trong trang tài khoản."
|
|
4067
|
+
},
|
|
4068
|
+
"style": {
|
|
4069
|
+
"width": "100%"
|
|
4070
|
+
}
|
|
4071
|
+
},
|
|
4072
|
+
"inspector": [
|
|
4073
|
+
{
|
|
4074
|
+
"tab": "general",
|
|
4075
|
+
"groups": [
|
|
4076
|
+
{
|
|
4077
|
+
"key": "receipt_content",
|
|
4078
|
+
"label": "Content",
|
|
4079
|
+
"controls": [
|
|
4080
|
+
"receipt_title",
|
|
4081
|
+
"receipt_number_label",
|
|
4082
|
+
"receipt_placed_label",
|
|
4083
|
+
"receipt_status_label",
|
|
4084
|
+
"receipt_items_label"
|
|
4085
|
+
]
|
|
4086
|
+
},
|
|
4087
|
+
{
|
|
4088
|
+
"key": "receipt_totals",
|
|
4089
|
+
"label": "Totals",
|
|
4090
|
+
"controls": [
|
|
4091
|
+
"receipt_total_label",
|
|
4092
|
+
"receipt_due_label"
|
|
4093
|
+
]
|
|
4094
|
+
},
|
|
4095
|
+
{
|
|
4096
|
+
"key": "receipt_unavailable",
|
|
4097
|
+
"label": "Unavailable",
|
|
4098
|
+
"controls": [
|
|
4099
|
+
"receipt_unavailable_text"
|
|
4100
|
+
]
|
|
4101
|
+
},
|
|
4102
|
+
{
|
|
4103
|
+
"key": "size",
|
|
4104
|
+
"label": "Size",
|
|
4105
|
+
"controls": [
|
|
4106
|
+
"width_select",
|
|
4107
|
+
"height_select",
|
|
4108
|
+
"size_bounds"
|
|
4109
|
+
]
|
|
4110
|
+
},
|
|
4111
|
+
{
|
|
4112
|
+
"key": "background",
|
|
4113
|
+
"label": "Background",
|
|
4114
|
+
"controls": [
|
|
4115
|
+
"bg_color",
|
|
4116
|
+
"bg_image"
|
|
4117
|
+
]
|
|
4118
|
+
},
|
|
4119
|
+
{
|
|
4120
|
+
"key": "shape",
|
|
4121
|
+
"label": "Shape",
|
|
4122
|
+
"controls": [
|
|
4123
|
+
"border",
|
|
4124
|
+
"corner",
|
|
4125
|
+
"shadow"
|
|
4126
|
+
]
|
|
4127
|
+
}
|
|
4128
|
+
]
|
|
4129
|
+
},
|
|
4130
|
+
{
|
|
4131
|
+
"tab": "advanced",
|
|
4132
|
+
"groups": [
|
|
4133
|
+
{
|
|
4134
|
+
"key": "spacing",
|
|
4135
|
+
"label": "Spacing",
|
|
4136
|
+
"controls": [
|
|
4137
|
+
"padding_margin"
|
|
4138
|
+
]
|
|
4139
|
+
},
|
|
4140
|
+
{
|
|
4141
|
+
"key": "display",
|
|
4142
|
+
"label": "Display",
|
|
4143
|
+
"controls": [
|
|
4144
|
+
"display"
|
|
4145
|
+
]
|
|
4146
|
+
},
|
|
4147
|
+
{
|
|
4148
|
+
"key": "animation",
|
|
4149
|
+
"label": "Animation",
|
|
4150
|
+
"controls": [
|
|
4151
|
+
"animation"
|
|
4152
|
+
]
|
|
4153
|
+
},
|
|
4154
|
+
{
|
|
4155
|
+
"key": "class_css",
|
|
4156
|
+
"label": "Class",
|
|
4157
|
+
"controls": [
|
|
4158
|
+
"class_css"
|
|
4159
|
+
]
|
|
4160
|
+
}
|
|
4161
|
+
]
|
|
4162
|
+
}
|
|
4163
|
+
],
|
|
4164
|
+
"controls": [
|
|
4165
|
+
"receipt_title",
|
|
4166
|
+
"receipt_number_label",
|
|
4167
|
+
"receipt_placed_label",
|
|
4168
|
+
"receipt_status_label",
|
|
4169
|
+
"receipt_items_label",
|
|
4170
|
+
"receipt_total_label",
|
|
4171
|
+
"receipt_due_label",
|
|
4172
|
+
"receipt_unavailable_text",
|
|
4173
|
+
"width_select",
|
|
4174
|
+
"height_select",
|
|
4175
|
+
"size_bounds",
|
|
4176
|
+
"bg_color",
|
|
4177
|
+
"bg_image",
|
|
4178
|
+
"border",
|
|
4179
|
+
"corner",
|
|
4180
|
+
"shadow",
|
|
4181
|
+
"padding_margin",
|
|
4182
|
+
"display",
|
|
4183
|
+
"animation",
|
|
4184
|
+
"class_css"
|
|
4185
|
+
],
|
|
4186
|
+
"description": "Shows the order a shopper has just paid for on a checkout completion page: order number, date, status, the items, the total, and anything still owed. Reads one order through the signed receipt grant the payment return endpoint puts in the completion URL, so it works for a guest who has no account.",
|
|
4187
|
+
"useWhen": [
|
|
4188
|
+
"Building the checkout completion page a shopper lands on after paying at a gateway.",
|
|
4189
|
+
"The store takes card or wallet payments and wants the buyer to see what they bought without signing in.",
|
|
4190
|
+
"Pairing with payment-status, which reports the MONEY; this reports the ORDER."
|
|
4191
|
+
],
|
|
4192
|
+
"avoidWhen": [
|
|
4193
|
+
"On any page other than the completion page — every other page is reached without the signed grant, so the block hides itself and renders nothing.",
|
|
4194
|
+
"As an order history list: this shows exactly one order. Use order-history for a signed-in shopper’s past orders.",
|
|
4195
|
+
"To show a home address or per-line prices — the receipt read is deliberately reduced and carries neither."
|
|
4196
|
+
],
|
|
4197
|
+
"contentTips": [
|
|
4198
|
+
"Word the labels the way the store talks about orders; every string here is yours, not the platform’s.",
|
|
4199
|
+
"The \"still owed\" row prints only when something is actually outstanding, so its label can say so plainly.",
|
|
4200
|
+
"The unavailable sentence is read by someone whose link has expired — point them at where the order really lives."
|
|
4201
|
+
],
|
|
4202
|
+
"semantics": [
|
|
4203
|
+
"order",
|
|
4204
|
+
"receipt",
|
|
4205
|
+
"checkout",
|
|
4206
|
+
"completion",
|
|
4207
|
+
"confirmation",
|
|
4208
|
+
"ecommerce",
|
|
4209
|
+
"guest"
|
|
4210
|
+
]
|
|
4211
|
+
},
|
|
3987
4212
|
"course-outline": {
|
|
3988
4213
|
"type": "course-outline",
|
|
3989
4214
|
"label": "Course Syllabus",
|
|
@@ -5576,6 +5801,29 @@ export const ELEMENTS = {
|
|
|
5576
5801
|
"locked": false,
|
|
5577
5802
|
"hideInLayer": false,
|
|
5578
5803
|
"childAllows": [],
|
|
5804
|
+
"events": {
|
|
5805
|
+
"form:submit": [
|
|
5806
|
+
"popup",
|
|
5807
|
+
"close_popup",
|
|
5808
|
+
"open_cart",
|
|
5809
|
+
"close_cart"
|
|
5810
|
+
],
|
|
5811
|
+
"form:success": [
|
|
5812
|
+
"go_to_url",
|
|
5813
|
+
"open_page",
|
|
5814
|
+
"popup",
|
|
5815
|
+
"close_popup",
|
|
5816
|
+
"open_cart",
|
|
5817
|
+
"close_cart",
|
|
5818
|
+
"go_to_checkout"
|
|
5819
|
+
],
|
|
5820
|
+
"form:error": [
|
|
5821
|
+
"popup",
|
|
5822
|
+
"close_popup",
|
|
5823
|
+
"open_cart",
|
|
5824
|
+
"close_cart"
|
|
5825
|
+
]
|
|
5826
|
+
},
|
|
5579
5827
|
"defaults": {
|
|
5580
5828
|
"specials": {
|
|
5581
5829
|
"formId": "",
|
|
@@ -5716,7 +5964,7 @@ export const ELEMENTS = {
|
|
|
5716
5964
|
"avoidWhen": [
|
|
5717
5965
|
"A single button or link is all that is needed — a form asks the visitor to type, which is a much larger request than a click.",
|
|
5718
5966
|
"The data belongs to an existing flow that already has its own surface (adding to the cart, checking out).",
|
|
5719
|
-
"
|
|
5967
|
+
"A `custom` form for sign-in or registration: use the LOGIN / REGISTER / FORGOT / RESET types instead. Storefront customer accounts exist — those types submit through /_wb/account/* rather than the submissions table, and a custom form pointed at the same job stores a password in a response row."
|
|
5720
5968
|
],
|
|
5721
5969
|
"contentTips": [
|
|
5722
5970
|
"Ask for the fewest fields that make the request answerable. Every additional field measurably reduces the number of people who finish.",
|
|
@@ -23374,6 +23622,13 @@ export const ELEMENTS = {
|
|
|
23374
23622
|
"locked": false,
|
|
23375
23623
|
"hideInLayer": false,
|
|
23376
23624
|
"childAllows": [],
|
|
23625
|
+
"events": {
|
|
23626
|
+
"click": [
|
|
23627
|
+
"go_to_url",
|
|
23628
|
+
"open_page",
|
|
23629
|
+
"popup"
|
|
23630
|
+
]
|
|
23631
|
+
},
|
|
23377
23632
|
"defaults": {
|
|
23378
23633
|
"style": {
|
|
23379
23634
|
"width": "100%",
|
|
@@ -32088,6 +32343,94 @@ export const TRAIT_WRITES = {
|
|
|
32088
32343
|
}
|
|
32089
32344
|
]
|
|
32090
32345
|
},
|
|
32346
|
+
"receipt_title": {
|
|
32347
|
+
"key": "receipt_title",
|
|
32348
|
+
"label": "Section title",
|
|
32349
|
+
"writes": [
|
|
32350
|
+
{
|
|
32351
|
+
"target": "specials",
|
|
32352
|
+
"writeKey": "title",
|
|
32353
|
+
"type": "string"
|
|
32354
|
+
}
|
|
32355
|
+
]
|
|
32356
|
+
},
|
|
32357
|
+
"receipt_number_label": {
|
|
32358
|
+
"key": "receipt_number_label",
|
|
32359
|
+
"label": "Order number label",
|
|
32360
|
+
"writes": [
|
|
32361
|
+
{
|
|
32362
|
+
"target": "specials",
|
|
32363
|
+
"writeKey": "numberLabel",
|
|
32364
|
+
"type": "string"
|
|
32365
|
+
}
|
|
32366
|
+
]
|
|
32367
|
+
},
|
|
32368
|
+
"receipt_placed_label": {
|
|
32369
|
+
"key": "receipt_placed_label",
|
|
32370
|
+
"label": "Date label",
|
|
32371
|
+
"writes": [
|
|
32372
|
+
{
|
|
32373
|
+
"target": "specials",
|
|
32374
|
+
"writeKey": "placedLabel",
|
|
32375
|
+
"type": "string"
|
|
32376
|
+
}
|
|
32377
|
+
]
|
|
32378
|
+
},
|
|
32379
|
+
"receipt_status_label": {
|
|
32380
|
+
"key": "receipt_status_label",
|
|
32381
|
+
"label": "Status label",
|
|
32382
|
+
"writes": [
|
|
32383
|
+
{
|
|
32384
|
+
"target": "specials",
|
|
32385
|
+
"writeKey": "statusLabel",
|
|
32386
|
+
"type": "string"
|
|
32387
|
+
}
|
|
32388
|
+
]
|
|
32389
|
+
},
|
|
32390
|
+
"receipt_items_label": {
|
|
32391
|
+
"key": "receipt_items_label",
|
|
32392
|
+
"label": "Items label",
|
|
32393
|
+
"writes": [
|
|
32394
|
+
{
|
|
32395
|
+
"target": "specials",
|
|
32396
|
+
"writeKey": "itemsLabel",
|
|
32397
|
+
"type": "string"
|
|
32398
|
+
}
|
|
32399
|
+
]
|
|
32400
|
+
},
|
|
32401
|
+
"receipt_total_label": {
|
|
32402
|
+
"key": "receipt_total_label",
|
|
32403
|
+
"label": "Total label",
|
|
32404
|
+
"writes": [
|
|
32405
|
+
{
|
|
32406
|
+
"target": "specials",
|
|
32407
|
+
"writeKey": "totalLabel",
|
|
32408
|
+
"type": "string"
|
|
32409
|
+
}
|
|
32410
|
+
]
|
|
32411
|
+
},
|
|
32412
|
+
"receipt_due_label": {
|
|
32413
|
+
"key": "receipt_due_label",
|
|
32414
|
+
"label": "Outstanding label",
|
|
32415
|
+
"writes": [
|
|
32416
|
+
{
|
|
32417
|
+
"target": "specials",
|
|
32418
|
+
"writeKey": "dueLabel",
|
|
32419
|
+
"type": "string"
|
|
32420
|
+
}
|
|
32421
|
+
]
|
|
32422
|
+
},
|
|
32423
|
+
"receipt_unavailable_text": {
|
|
32424
|
+
"key": "receipt_unavailable_text",
|
|
32425
|
+
"label": "Unavailable message",
|
|
32426
|
+
"writes": [
|
|
32427
|
+
{
|
|
32428
|
+
"target": "specials",
|
|
32429
|
+
"writeKey": "unavailableText",
|
|
32430
|
+
"type": "string"
|
|
32431
|
+
}
|
|
32432
|
+
]
|
|
32433
|
+
},
|
|
32091
32434
|
"member_audience": {
|
|
32092
32435
|
"key": "member_audience",
|
|
32093
32436
|
"label": "Show to",
|
package/dist/core/tree.js
CHANGED
|
@@ -4,6 +4,15 @@ export const SPEC_OVERLAY_ID = 'overlayId';
|
|
|
4
4
|
/** The specials stamps a composed global section carries. */
|
|
5
5
|
export const SPEC_GLOBAL_ID = 'globalId';
|
|
6
6
|
export const SPEC_GLOBAL_KIND = 'globalKind';
|
|
7
|
+
/**
|
|
8
|
+
* The OPTIMISTIC FENCE on a shared master, one per kind.
|
|
9
|
+
*
|
|
10
|
+
* Compose writes it, the save sends it back as `expectRev`, and the platform
|
|
11
|
+
* refuses a stale one with a warning and a 200 — so a client that does not
|
|
12
|
+
* re-stamp from the save's own report silently drops every edit after the first.
|
|
13
|
+
*/
|
|
14
|
+
export const SPEC_GLOBAL_REV = 'globalRev';
|
|
15
|
+
export const SPEC_OVERLAY_REV = 'overlayRev';
|
|
7
16
|
export const SPEC_GLOBAL_REF = 'globalRef';
|
|
8
17
|
/** The specials stamps a composed APP BLOCK carries (appblocks.go:97-100). */
|
|
9
18
|
export const SPEC_APP_BLOCK_ID = 'appBlockId';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isOverlay, subtreeIds, ancestors, appBlockRoot, SPEC_GLOBAL_ID, SPEC_GLOBAL_KIND, SPEC_GLOBAL_REF, } from '../../core/tree.js';
|
|
2
2
|
import { ELEMENTS, ELEMENT_SEEDS, SATELLITE_RULES } from '../../catalog/elements.generated.js';
|
|
3
|
-
import { createNode, mintSatellites } from './node.js';
|
|
3
|
+
import { bindingsForConfig, createNode, mintSatellites } from './node.js';
|
|
4
4
|
import { refuseSecondTemplate } from './traps.js';
|
|
5
5
|
import { genId } from './ids.js';
|
|
6
6
|
/** Append sentinel: splice clamps a too-large index, and `isSyncablePatch`
|
|
@@ -252,17 +252,13 @@ function rebindPatch(doc, id, keys) {
|
|
|
252
252
|
if (!touchesAxis)
|
|
253
253
|
return null;
|
|
254
254
|
const node = doc.node(id);
|
|
255
|
-
const meta = ELEMENTS[node.data.type];
|
|
256
|
-
const table = meta?.bindingsFor;
|
|
257
|
-
if (!table)
|
|
258
|
-
return null;
|
|
259
255
|
const cfg = node.config ?? {};
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const next =
|
|
256
|
+
// The config the node will HAVE once these keys land — the axis is read off
|
|
257
|
+
// the result of the write, not off either half of it.
|
|
258
|
+
const next = bindingsForConfig(node.data.type, { ...cfg, ...keys, datasetSource: keys.datasetSource ?? cfg.datasetSource ?? 'product' });
|
|
263
259
|
if (!next)
|
|
264
260
|
return null;
|
|
265
|
-
return { op: 'set', path: ['nodes', id, 'bindings'], value:
|
|
261
|
+
return { op: 'set', path: ['nodes', id, 'bindings'], value: next };
|
|
266
262
|
}
|
|
267
263
|
/**
|
|
268
264
|
* Copy a node and everything under it, under fresh ids, beside the original.
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { applyPatches } from '../../core/patch.js';
|
|
2
2
|
import { childrenOf, isOverlay, appBlockRoot } from '../../core/tree.js';
|
|
3
3
|
import { bandOf, isGlobal } from './traps.js';
|
|
4
|
+
import { SATELLITE_RULES } from '../../catalog/elements.generated.js';
|
|
5
|
+
/**
|
|
6
|
+
* The satellite nodes this one owns, as {id, config key} pairs.
|
|
7
|
+
*
|
|
8
|
+
* Read from the GENERATED rules rather than a hand-kept list, so an element the
|
|
9
|
+
* platform gives a satellite is covered the day the catalog is regenerated. A
|
|
10
|
+
* key pointing at an id the document does not hold is skipped rather than
|
|
11
|
+
* reported: a dangling pointer is `validateForSave`'s business, not the map's.
|
|
12
|
+
*/
|
|
13
|
+
function satellitesOf(doc, id) {
|
|
14
|
+
const node = doc.nodes[id];
|
|
15
|
+
const rules = SATELLITE_RULES[node?.data?.type ?? ''] ?? [];
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const rule of rules) {
|
|
18
|
+
const satId = node?.config?.[rule.configKey];
|
|
19
|
+
if (typeof satId === 'string' && satId && doc.nodes[satId]) {
|
|
20
|
+
out.push({ id: satId, key: rule.configKey });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
4
25
|
/**
|
|
5
26
|
* One page's document, in memory.
|
|
6
27
|
*
|
|
@@ -139,8 +160,19 @@ export class PageDoc {
|
|
|
139
160
|
out.global = true;
|
|
140
161
|
if (appBlockRoot(this.doc, id) === id)
|
|
141
162
|
out.app = true;
|
|
142
|
-
if (level + 1 < depth
|
|
143
|
-
|
|
163
|
+
if (level + 1 < depth) {
|
|
164
|
+
// Satellites come FIRST, and are counted apart from `children`: they are
|
|
165
|
+
// this node's own chrome, not content in it, and mixing the two would
|
|
166
|
+
// make the child count disagree with `data.nodes` — the number every
|
|
167
|
+
// index-taking call (sb_add, sb_move) is written against.
|
|
168
|
+
const sats = satellitesOf(this.doc, id).map((sat) => {
|
|
169
|
+
const l = line(sat.id, level + 1);
|
|
170
|
+
l.satellite = sat.key;
|
|
171
|
+
return l;
|
|
172
|
+
});
|
|
173
|
+
const kids = kidIds.map((k) => line(k, level + 1));
|
|
174
|
+
if (sats.length || kids.length)
|
|
175
|
+
out.kids = [...sats, ...kids];
|
|
144
176
|
}
|
|
145
177
|
return out;
|
|
146
178
|
};
|
|
@@ -5,6 +5,43 @@ import { genId } from './ids.js';
|
|
|
5
5
|
function copy(v) {
|
|
6
6
|
return JSON.parse(JSON.stringify(v));
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* The bindings an element carries for a given config, or null when its data
|
|
10
|
+
* axis says nothing about this pair.
|
|
11
|
+
*
|
|
12
|
+
* Reads the generated `bindingsFor` table — the platform's own
|
|
13
|
+
* `datasetBindings(type, config)` answers, enumerated at codegen.
|
|
14
|
+
*
|
|
15
|
+
* THE KIND FALLBACK IS THE POINT. The table is keyed `${datasetSource}|${kind}`,
|
|
16
|
+
* but TEN elements with a table declare no default `config.kind` at all —
|
|
17
|
+
* `list-dataset`, `dataset-block`, `media-dataset`, `collection-media`,
|
|
18
|
+
* `product-variants`, `quantity-dataset` and friends, which is most of what a
|
|
19
|
+
* store is built from. For those the key came out `category|`, matched nothing,
|
|
20
|
+
* and the lookup returned null: a `list-dataset` switched to collections kept
|
|
21
|
+
* `bind-target` → `product_list` and repeated PRODUCTS under a heading that said
|
|
22
|
+
* collections. Every `<source>|*` row of such an element is identical (they have
|
|
23
|
+
* no kind axis to vary on), so the first row for the source IS the answer, and
|
|
24
|
+
* falling back to it is what makes the axis reachable at all.
|
|
25
|
+
*/
|
|
26
|
+
export function bindingsForConfig(type, config) {
|
|
27
|
+
const table = ELEMENTS[type]?.bindingsFor;
|
|
28
|
+
if (!table)
|
|
29
|
+
return null;
|
|
30
|
+
const source = String(config?.datasetSource ?? '');
|
|
31
|
+
if (!source)
|
|
32
|
+
return null;
|
|
33
|
+
const kind = String(config?.kind ?? '');
|
|
34
|
+
const exact = table[`${source}|${kind}`];
|
|
35
|
+
if (exact)
|
|
36
|
+
return copy(exact);
|
|
37
|
+
// No kind axis on this element: any row for the source carries the same answer.
|
|
38
|
+
const prefix = `${source}|`;
|
|
39
|
+
for (const [key, value] of Object.entries(table)) {
|
|
40
|
+
if (key.startsWith(prefix))
|
|
41
|
+
return copy(value);
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
8
45
|
/**
|
|
9
46
|
* Mint a node seeded from its element's catalog defaults.
|
|
10
47
|
*
|
|
@@ -21,6 +58,7 @@ export function createNode(type, opts = {}) {
|
|
|
21
58
|
}
|
|
22
59
|
const d = meta.defaults;
|
|
23
60
|
const states = d.states;
|
|
61
|
+
const config = { ...copy(d.config ?? {}), ...(opts.config ?? {}) };
|
|
24
62
|
return {
|
|
25
63
|
id: genId(type),
|
|
26
64
|
data: {
|
|
@@ -33,7 +71,7 @@ export function createNode(type, opts = {}) {
|
|
|
33
71
|
custom: {},
|
|
34
72
|
},
|
|
35
73
|
style: { ...copy(d.style ?? {}), ...(opts.style ?? {}) },
|
|
36
|
-
config
|
|
74
|
+
config,
|
|
37
75
|
specials: { ...copy(d.specials ?? {}), ...(opts.specials ?? {}) },
|
|
38
76
|
responsive: copy(d.responsive ?? {}),
|
|
39
77
|
...(states ? { states: copy(states) } : {}),
|
|
@@ -49,12 +87,17 @@ export function createNode(type, opts = {}) {
|
|
|
49
87
|
// placeholder forever. Found on a live page: four product cards reading
|
|
50
88
|
// "$0.00" with no titles, on a catalogue that had four products.
|
|
51
89
|
//
|
|
52
|
-
// Codegen bakes the factory's answer for
|
|
53
|
-
// overrides `config.kind` or
|
|
54
|
-
//
|
|
55
|
-
// the
|
|
56
|
-
//
|
|
57
|
-
|
|
90
|
+
// Codegen bakes the factory's answer for EVERY config pair, not just the
|
|
91
|
+
// default one, so a caller who overrides `config.kind` or
|
|
92
|
+
// `config.datasetSource` in the same call is answered here rather than left
|
|
93
|
+
// holding the default entity's bindings. It used to be left: `sb_add` with
|
|
94
|
+
// `datasetSource: "category"` minted a repeater still bound to
|
|
95
|
+
// `product_list`, saved, published, and repeated products under a heading
|
|
96
|
+
// that said collections — with no warning on any of the four steps, because
|
|
97
|
+
// a wrong binding renders exactly like a right one until you read the data.
|
|
98
|
+
// `sb_set` re-derives on the same table for the same reason; the two paths
|
|
99
|
+
// must not disagree about what a config means.
|
|
100
|
+
bindings: bindingsForConfig(type, config) ?? copy(d.bindings ?? []),
|
|
58
101
|
};
|
|
59
102
|
}
|
|
60
103
|
/**
|
|
@@ -110,6 +110,47 @@ export function readinessGaps(input) {
|
|
|
110
110
|
fix: 'Add at least one shipping method (sb_api_find "shipping methods").',
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
|
+
// THE OTHER FIXED PATHS.
|
|
114
|
+
//
|
|
115
|
+
// `page.FixedPathTypes` is four — search, checkout, complete, account — and
|
|
116
|
+
// each resolves to the site's PUBLISHED page of that type
|
|
117
|
+
// (storefront/typeroute.go). `complete` is the one that backstops itself: with
|
|
118
|
+
// no completion page the storefront serves a built-in receipt, measured 200 on
|
|
119
|
+
// a live store that had none. The other three 404, and `/account` and
|
|
120
|
+
// `/search` 404 QUIETLY — nothing links to them by default, so the merchant
|
|
121
|
+
// finds out when a shopper who wants their order history does.
|
|
122
|
+
//
|
|
123
|
+
// Separate from the five above, and after them, because these do not stand
|
|
124
|
+
// between the store and a PAID ORDER: a shop with no account page still takes
|
|
125
|
+
// money. They stand between it and a finished website, which is the next
|
|
126
|
+
// question a merchant asks.
|
|
127
|
+
for (const [type, id, what, fix] of [
|
|
128
|
+
[
|
|
129
|
+
'account',
|
|
130
|
+
'accountPage',
|
|
131
|
+
'/account 404s. A shopper has no way to see their orders, addresses or saved items, and ' +
|
|
132
|
+
'the account elements (account-info, address-book, wishlist-list, points-card) have ' +
|
|
133
|
+
'nowhere to live.',
|
|
134
|
+
'Create a page of type "account" and publish it. Put login and register forms behind a ' +
|
|
135
|
+
'member-gate with audience "guests", and the profile behind audience "members".',
|
|
136
|
+
],
|
|
137
|
+
[
|
|
138
|
+
'search',
|
|
139
|
+
'searchPage',
|
|
140
|
+
'/search 404s, so a search box in the header sends every shopper to a dead page.',
|
|
141
|
+
'Create a page of type "search" and publish it.',
|
|
142
|
+
],
|
|
143
|
+
]) {
|
|
144
|
+
if (!pages || published(pages, type))
|
|
145
|
+
continue;
|
|
146
|
+
const draft = drafted(pages, type);
|
|
147
|
+
gaps.push({
|
|
148
|
+
id,
|
|
149
|
+
draft,
|
|
150
|
+
problem: what,
|
|
151
|
+
fix: draft ? `Publish the ${type} page that already exists.` : fix,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
113
154
|
if (input.globalNodes !== null && !opensCart([...input.pageNodes, ...input.globalNodes])) {
|
|
114
155
|
gaps.push({
|
|
115
156
|
id: 'cartTrigger',
|