@fayz-ai/storefront 0.80.0 → 0.82.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/dist/analytics/bus.d.ts +3 -1
- package/dist/analytics/bus.d.ts.map +1 -1
- package/dist/analytics/posthog.d.ts.map +1 -1
- package/dist/app/prerender.d.ts +1 -9
- package/dist/app/prerender.d.ts.map +1 -1
- package/dist/app/router.d.ts +1 -1
- package/dist/app/sitemap.d.ts +10 -0
- package/dist/app/sitemap.d.ts.map +1 -0
- package/dist/blocks/registry.d.ts.map +1 -1
- package/dist/chunk-746BEU7H.js +160 -0
- package/dist/chunk-746BEU7H.js.map +1 -0
- package/dist/chunk-GI5BTJK6.js +1336 -0
- package/dist/chunk-GI5BTJK6.js.map +1 -0
- package/dist/{chunk-V7CGLOEN.js → chunk-RYWQGQ4M.js} +367 -2740
- package/dist/chunk-RYWQGQ4M.js.map +1 -0
- package/dist/chunk-UJHCVRK3.js +1184 -0
- package/dist/chunk-UJHCVRK3.js.map +1 -0
- package/dist/chunk-WCAQRATH.js +30 -0
- package/dist/chunk-WCAQRATH.js.map +1 -0
- package/dist/components/delivery/StoreCover.d.ts.map +1 -1
- package/dist/data/menu-needs.d.ts +30 -0
- package/dist/data/menu-needs.d.ts.map +1 -0
- package/dist/document/config.d.ts +22 -0
- package/dist/document/config.d.ts.map +1 -1
- package/dist/edge.d.ts +2 -0
- package/dist/edge.d.ts.map +1 -1
- package/dist/edge.js +137 -1
- package/dist/edge.js.map +1 -1
- package/dist/experiments/assign.d.ts +113 -0
- package/dist/experiments/assign.d.ts.map +1 -0
- package/dist/experiments/react.d.ts +98 -0
- package/dist/experiments/react.d.ts.map +1 -0
- package/dist/host/edge-handler.d.ts +65 -0
- package/dist/host/edge-handler.d.ts.map +1 -0
- package/dist/host/image-origin.d.ts +16 -0
- package/dist/host/image-origin.d.ts.map +1 -0
- package/dist/host/store-host.d.ts +141 -0
- package/dist/host/store-host.d.ts.map +1 -0
- package/dist/host.d.ts +9 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +229 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/render.js +7 -27
- package/dist/render.js.map +1 -1
- package/dist/tanstack.d.ts +35 -0
- package/dist/tanstack.d.ts.map +1 -0
- package/dist/tanstack.js +116 -0
- package/dist/tanstack.js.map +1 -0
- package/dist/views/MenuPage.d.ts +21 -6
- package/dist/views/MenuPage.d.ts.map +1 -1
- package/package.json +17 -2
- package/dist/chunk-V7CGLOEN.js.map +0 -1
|
@@ -0,0 +1,1336 @@
|
|
|
1
|
+
import { STOREFRONT_SLOTS, registerStorefrontBlocks, THEME_SCALE_FIELDS, THEME_SCALE_VAR_PREFIX } from './chunk-RYWQGQ4M.js';
|
|
2
|
+
import { toBlockNodes, routePathParams } from './chunk-UJHCVRK3.js';
|
|
3
|
+
import { listEntityDeclarations, resolveHandle, errorMessage, RESERVED_ENTITY_FIELDS, ENTITY_FIELD_TYPES, handleNameFor, registerHandle, checkBlockConstraints, hasHandle, listHandles, blockRegistry, getBlockMeta, loaderRegistry, listLoaders, isLoaderParamRef, isLoaderPropRef } from '@fayz-ai/core';
|
|
4
|
+
import { PROMOTION_TYPES, PROMOTION_STATUSES, APPLICATION_METHOD_TYPES, APPLICATION_METHOD_TARGET_TYPES, APPLICATION_METHOD_ALLOCATIONS, hasRuleAttribute, listRuleAttributeKeys, RULE_OPERATORS, resolveRuleAttribute } from '@fayz-ai/shop/rules';
|
|
5
|
+
|
|
6
|
+
// src/document/sections.ts
|
|
7
|
+
var STORE_DOCUMENT_KIND = "fayz.store";
|
|
8
|
+
var STORE_COMMERCE_MODES = ["checkout", "catalog", "enquiry", "delivery"];
|
|
9
|
+
var STORE_PAYMENT_MODES = ["mock", "pix-mercadopago", "gateway"];
|
|
10
|
+
var STORE_PAYMENT_METHOD_KINDS = [
|
|
11
|
+
"pix",
|
|
12
|
+
"credit_card",
|
|
13
|
+
"debit_card",
|
|
14
|
+
"boleto",
|
|
15
|
+
"cash",
|
|
16
|
+
"other"
|
|
17
|
+
];
|
|
18
|
+
var STORE_BACKEND_PROVIDERS = ["mock", "fayz-api", "fayz-shop", "custom"];
|
|
19
|
+
var STORE_ROUTE_KINDS = [
|
|
20
|
+
"home",
|
|
21
|
+
"catalog",
|
|
22
|
+
"product",
|
|
23
|
+
"checkout",
|
|
24
|
+
"account",
|
|
25
|
+
"order",
|
|
26
|
+
"custom"
|
|
27
|
+
];
|
|
28
|
+
var STORE_ROUTE_CHROMES = ["default", "minimal", "focused"];
|
|
29
|
+
var STORE_THEME_RADII = ["none", "soft", "round", "pill"];
|
|
30
|
+
var f = (type, description, extra = {}) => ({ type, description, ...extra });
|
|
31
|
+
var STORE_SECTIONS = {
|
|
32
|
+
store: {
|
|
33
|
+
description: "Store identity: name, currency, locale.",
|
|
34
|
+
versions: [1],
|
|
35
|
+
fields: {
|
|
36
|
+
name: f("string", "Store display name (header, page title).", { required: true }),
|
|
37
|
+
currency: f("string", "ISO 4217 currency code \u2014 default 'BRL'."),
|
|
38
|
+
locale: f("string", "Intl locale for money/date formatting \u2014 default 'pt-BR'.")
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
theme: {
|
|
42
|
+
description: "Visual identity: colors, fonts, radius, header/card personality.",
|
|
43
|
+
versions: [1],
|
|
44
|
+
fields: {
|
|
45
|
+
name: f("string", "Theme name.", { required: true }),
|
|
46
|
+
colors: f("object", "HSL token triplets (background, primary, \u2026)."),
|
|
47
|
+
font: f("object", "Google Fonts pairing (heading, body, display, serif)."),
|
|
48
|
+
radius: f("string", "Corner radius scale.", { enum: STORE_THEME_RADII }),
|
|
49
|
+
header: f("object", "Header treatment (variant, uppercaseNav, search)."),
|
|
50
|
+
productCard: f("object", "Card style and image aspect."),
|
|
51
|
+
uppercaseButtons: f("boolean", "Uppercase CTA buttons with letter-spacing."),
|
|
52
|
+
spacing: f("object", "Spacing scale (key \u2192 CSS length), emitted as --sf-space-<key>. e.g. {sm:'8px', md:'16px'}."),
|
|
53
|
+
typeScale: f("object", "Type scale (key \u2192 CSS font-size), emitted as --sf-text-<key>. e.g. {body:'1rem', h1:'clamp(40px,6vw,66px)'}."),
|
|
54
|
+
breakpoints: f("object", "Layout breakpoints (key \u2192 CSS length), emitted as --sf-breakpoint-<key> for JS/generated-style consumers."),
|
|
55
|
+
motion: f("object", "Motion tokens (key \u2192 duration or easing), emitted as --sf-motion-<key>. e.g. {fast:'150ms', reveal:'cubic-bezier(0.22,1,0.36,1)'}."),
|
|
56
|
+
tokens: f("object", "Free-form brand-token passthrough, emitted verbatim as CSS custom properties.")
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
commerce: {
|
|
60
|
+
description: "Commercial behavior: mode, shipping, payments, discounts, enquiry, feature toggles.",
|
|
61
|
+
// v2 = the legacy `discounts` coupon list has been migrated onto the rule
|
|
62
|
+
// engine (codemod commerce.discounts-to-promotions). Both versions are
|
|
63
|
+
// understood, which is the whole point: the fleet migrates store by store.
|
|
64
|
+
versions: [1, 2],
|
|
65
|
+
fields: {
|
|
66
|
+
commerceMode: f("string", "Checkout-first, catalog-only browsing, or product enquiry.", {
|
|
67
|
+
enum: STORE_COMMERCE_MODES
|
|
68
|
+
}),
|
|
69
|
+
shipping: f("object", "Flat rate and free-above threshold."),
|
|
70
|
+
payments: f("object", "Payment mode and offered methods."),
|
|
71
|
+
discounts: f("array", "DEPRECATED \u2014 store-level discount codes ({code, percent, title?}). Use promotions.promotions.", {
|
|
72
|
+
deprecated: {
|
|
73
|
+
id: "storefront.commerce.discounts",
|
|
74
|
+
removedAtVersion: 2,
|
|
75
|
+
replacement: "promotions.promotions"
|
|
76
|
+
}
|
|
77
|
+
}),
|
|
78
|
+
enquiry: f("object", "Product enquiry behavior for catalog/enquiry stores."),
|
|
79
|
+
features: f("object", "Feature toggles (discounts, accounts, cart, checkout).")
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
presentation: {
|
|
83
|
+
description: "What the shopper sees: composed pages, slots, nav, footer, chrome.",
|
|
84
|
+
versions: [1],
|
|
85
|
+
fields: {
|
|
86
|
+
announcement: f("string", "Promo message above the header."),
|
|
87
|
+
logo: f("string", "Text logo. A JSX logo is code and lives at code.logo."),
|
|
88
|
+
home: f("object", "Home page blueprint (legacy short form of pages.home)."),
|
|
89
|
+
pages: f("object", "Pages composed from blocks, keyed by page kind."),
|
|
90
|
+
slots: f("object", "Blocks grafted into named positions inside SDK pages."),
|
|
91
|
+
nav: f("array", "Header nav links ({label, to})."),
|
|
92
|
+
footer: f("object", "Footer content (about, contact, social, labels)."),
|
|
93
|
+
imageLoading: f("object", "Global image reveal behavior."),
|
|
94
|
+
catalogFacets: f(
|
|
95
|
+
"array",
|
|
96
|
+
"Catalog refinements over product.metadata ({key, label, values[]}), rendered in the filters panel and mirrored into ?<key>=<value>."
|
|
97
|
+
),
|
|
98
|
+
productSpecs: f(
|
|
99
|
+
"object",
|
|
100
|
+
"PDP specification table rules: `exclude` hides metadata keys, `labels` renames the rest."
|
|
101
|
+
),
|
|
102
|
+
search: f(
|
|
103
|
+
"object",
|
|
104
|
+
"Header search: mode ('inline' live filter, the default, or 'overlay'), plus placeholder, resultsPath, suggestionsLimit, minChars and labels. A custom suggestion renderer is code and lives at code.searchRenderSuggestion."
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
content: {
|
|
109
|
+
description: "Long-form content: legal/institutional pages, product reviews.",
|
|
110
|
+
versions: [1],
|
|
111
|
+
fields: {
|
|
112
|
+
legal: f("object", "Privacy / terms / returns page text."),
|
|
113
|
+
reviews: f(
|
|
114
|
+
"object",
|
|
115
|
+
"Store-supplied product reviews ({items, enabled, emptyText}). Nothing is invented \u2014 with no items the PDP renders an honest empty state."
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
catalog: {
|
|
120
|
+
description: "Mock catalog seed for stores without a live backend.",
|
|
121
|
+
versions: [1],
|
|
122
|
+
fields: {
|
|
123
|
+
products: f("array", "Seed products."),
|
|
124
|
+
categories: f("array", "Seed categories."),
|
|
125
|
+
discounts: f("array", "Seed discounts."),
|
|
126
|
+
promotions: f("array", "Seed rule-engine promotions for the mock provider. Prefer the top-level promotions section.")
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
entities: {
|
|
130
|
+
description: "Custom entity declarations (defineEntity inputs), keyed by entity key.",
|
|
131
|
+
versions: [1],
|
|
132
|
+
fields: {
|
|
133
|
+
definitions: f("object", "Entity key \u2192 declaration ({fields, scope, access, indexes, \u2026}).")
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
promotions: {
|
|
137
|
+
description: "Rule-engine promotions: eligibility rules + application method, as pure data (spine M6).",
|
|
138
|
+
versions: [1],
|
|
139
|
+
fields: {
|
|
140
|
+
promotions: f("array", "Promotions ({id, code?, isAutomatic?, rules?, applicationMethod}). Rule attributes come from the rule_attribute vocabulary.")
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
backend: {
|
|
144
|
+
description: "Backend descriptor: which provider serves the store.",
|
|
145
|
+
// v2 = the legacy supabaseUrl/supabaseAnonKey pair has moved to
|
|
146
|
+
// auth.supabase (codemod backend.supabase-to-auth).
|
|
147
|
+
versions: [1, 2],
|
|
148
|
+
fields: {
|
|
149
|
+
provider: f("string", "Backend provider family.", { enum: STORE_BACKEND_PROVIDERS }),
|
|
150
|
+
url: f("string", "Backend URL, when the provider needs one."),
|
|
151
|
+
supabaseUrl: f("string", "DEPRECATED legacy field, kept for lossless round-trip. Use auth.supabase.url.", {
|
|
152
|
+
deprecated: {
|
|
153
|
+
id: "storefront.backend.supabaseUrl",
|
|
154
|
+
removedAtVersion: 2,
|
|
155
|
+
replacement: "auth.supabase.url"
|
|
156
|
+
}
|
|
157
|
+
}),
|
|
158
|
+
supabaseAnonKey: f("string", "DEPRECATED legacy field, kept for lossless round-trip. Use auth.supabase.anonKey.", {
|
|
159
|
+
deprecated: {
|
|
160
|
+
id: "storefront.backend.supabaseAnonKey",
|
|
161
|
+
removedAtVersion: 2,
|
|
162
|
+
replacement: "auth.supabase.anonKey"
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
auth: {
|
|
168
|
+
description: "Customer auth, when expressible as data. Code-bearing auth lives at code.auth.",
|
|
169
|
+
versions: [1],
|
|
170
|
+
fields: {
|
|
171
|
+
adapter: f("string", "Adapter id ('mock' | 'supabase')."),
|
|
172
|
+
provider: f("string", "Provider id ('mock' | 'supabase')."),
|
|
173
|
+
routes: f("object", "Auth route overrides."),
|
|
174
|
+
supabase: f("object", "Supabase connection (url, anonKey, callback urls).")
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
code: {
|
|
178
|
+
description: "The code sidecar: config fields that hold code, referenced by registered handle name.",
|
|
179
|
+
versions: [1],
|
|
180
|
+
fields: {
|
|
181
|
+
logo: f("string", "Handle of the JSX logo node."),
|
|
182
|
+
provider: f("string", "Handle of the explicit ShopProvider."),
|
|
183
|
+
auth: f("string", "Handle of a code-bearing auth config."),
|
|
184
|
+
footerCredit: f("string", "Handle of the footer credit node."),
|
|
185
|
+
searchRenderSuggestion: f("string", "Handle of the custom search-suggestion renderer."),
|
|
186
|
+
components: f("object", "Component-slot name \u2192 handle of its override."),
|
|
187
|
+
routes: f("array", "Custom routes; each component is a handle name.")
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
var STORE_SECTION_NAMES = Object.keys(STORE_SECTIONS);
|
|
192
|
+
function sectionToJsonSchema(name) {
|
|
193
|
+
const spec = STORE_SECTIONS[name];
|
|
194
|
+
const properties = {
|
|
195
|
+
version: {
|
|
196
|
+
type: "integer",
|
|
197
|
+
description: `Schema version of the ${name} section. Supported: ${spec.versions.join(", ")}.`,
|
|
198
|
+
enum: [...spec.versions]
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
const required = ["version"];
|
|
202
|
+
for (const [field, fieldSpec] of Object.entries(spec.fields)) {
|
|
203
|
+
const node = { type: fieldSpec.type, description: fieldSpec.description };
|
|
204
|
+
if (fieldSpec.enum) node.enum = [...fieldSpec.enum];
|
|
205
|
+
if (fieldSpec.deprecated) {
|
|
206
|
+
node.deprecated = true;
|
|
207
|
+
node["x-fayz-deprecation"] = fieldSpec.deprecated.id;
|
|
208
|
+
node["x-fayz-replacement"] = fieldSpec.deprecated.replacement;
|
|
209
|
+
node["x-fayz-removed-at-section-version"] = fieldSpec.deprecated.removedAtVersion;
|
|
210
|
+
}
|
|
211
|
+
properties[field] = node;
|
|
212
|
+
if (fieldSpec.required) required.push(field);
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
type: "object",
|
|
216
|
+
description: spec.description,
|
|
217
|
+
properties,
|
|
218
|
+
required
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function isJsonValue(value) {
|
|
222
|
+
if (value === null) return true;
|
|
223
|
+
const t = typeof value;
|
|
224
|
+
if (t === "string" || t === "number" || t === "boolean") return true;
|
|
225
|
+
if (Array.isArray(value)) return value.every(isJsonValue);
|
|
226
|
+
if (t === "object") {
|
|
227
|
+
const record = value;
|
|
228
|
+
if ("$$typeof" in record) return false;
|
|
229
|
+
const proto = Object.getPrototypeOf(record);
|
|
230
|
+
if (proto !== Object.prototype && proto !== null) return false;
|
|
231
|
+
return Object.values(record).every((v) => v === void 0 || isJsonValue(v));
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
function isRecord(value) {
|
|
236
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
237
|
+
}
|
|
238
|
+
function compact(obj) {
|
|
239
|
+
const out = {};
|
|
240
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
241
|
+
if (value !== void 0) out[key] = value;
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/extend/component-contracts.ts
|
|
247
|
+
var STOREFRONT_COMPONENT_KEYS = [
|
|
248
|
+
"ProductCard",
|
|
249
|
+
"ProductDetail",
|
|
250
|
+
"ProductGallery",
|
|
251
|
+
"ProductPrice",
|
|
252
|
+
"ProductActions",
|
|
253
|
+
"ProductEnquiryForm",
|
|
254
|
+
"CategoryCard",
|
|
255
|
+
"CollectionSection",
|
|
256
|
+
"HeroSection",
|
|
257
|
+
"MediaCarousel",
|
|
258
|
+
"ProductSlider",
|
|
259
|
+
"Header",
|
|
260
|
+
"Footer",
|
|
261
|
+
"Shell",
|
|
262
|
+
"EmptyState",
|
|
263
|
+
"LoadingState",
|
|
264
|
+
"ErrorState",
|
|
265
|
+
"CartDrawerFooterExtra"
|
|
266
|
+
];
|
|
267
|
+
var STOREFRONT_PAGE_KINDS = ["home", "catalog", "product", "collection", "content"];
|
|
268
|
+
var STRUCTURAL_BLOCK_KEYS = /* @__PURE__ */ new Set(["type", "id", "props", "children", "data"]);
|
|
269
|
+
var DATA_NEED_KEYS = ["loader", "params"];
|
|
270
|
+
var CODE_ROUTE_KEYS = ["id", "path", "kind", "chrome", "component"];
|
|
271
|
+
var SLOT_AREAS = [...new Set(STOREFRONT_SLOTS.map((slot) => slot.area))];
|
|
272
|
+
function slotNamesFor(area) {
|
|
273
|
+
return STOREFRONT_SLOTS.filter((slot) => slot.area === area).map((slot) => slot.name);
|
|
274
|
+
}
|
|
275
|
+
var PAGE_KIND_PARAMS = {
|
|
276
|
+
home: [],
|
|
277
|
+
catalog: [],
|
|
278
|
+
product: ["slug"],
|
|
279
|
+
collection: ["slug"]
|
|
280
|
+
};
|
|
281
|
+
var SLOT_AREA_PARAMS = {
|
|
282
|
+
productDetail: ["slug"]
|
|
283
|
+
};
|
|
284
|
+
var knownEntities = /* @__PURE__ */ new Set();
|
|
285
|
+
function checkEntityRef(errors, path, value) {
|
|
286
|
+
if (typeof value !== "string" || !value) return;
|
|
287
|
+
if (!knownEntities.has(value)) {
|
|
288
|
+
push(
|
|
289
|
+
errors,
|
|
290
|
+
path,
|
|
291
|
+
"unknown-entity",
|
|
292
|
+
`entity "${value}" is not declared (defineEntity or the entities section)`,
|
|
293
|
+
[...knownEntities].sort()
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function push(errors, path, code, message, valid) {
|
|
298
|
+
errors.push(valid ? { path, code, message, valid: [...valid] } : { path, code, message });
|
|
299
|
+
}
|
|
300
|
+
function blockPropKeys(entry) {
|
|
301
|
+
if ("props" in entry) {
|
|
302
|
+
const props = entry.props;
|
|
303
|
+
return isRecord(props) ? Object.keys(props) : [];
|
|
304
|
+
}
|
|
305
|
+
return Object.keys(entry).filter((key) => !STRUCTURAL_BLOCK_KEYS.has(key));
|
|
306
|
+
}
|
|
307
|
+
function validateSettingValues(errors, valuesPath, settings, values, owner, labelPrefix = "") {
|
|
308
|
+
for (const setting of settings) {
|
|
309
|
+
const value = values[setting.id];
|
|
310
|
+
if (value === void 0) continue;
|
|
311
|
+
const valuePath = `${valuesPath}.${setting.id}`;
|
|
312
|
+
const label = `${labelPrefix}${setting.id}`;
|
|
313
|
+
if (setting.type === "entity") {
|
|
314
|
+
checkEntityRef(errors, valuePath, value);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (setting.type !== "list" || !setting.of?.length || !Array.isArray(value)) continue;
|
|
318
|
+
const of = setting.of;
|
|
319
|
+
const declared = collectSettingIds(of);
|
|
320
|
+
value.forEach((item, index) => {
|
|
321
|
+
if (!isRecord(item)) return;
|
|
322
|
+
const itemPath = `${valuePath}[${index}]`;
|
|
323
|
+
for (const key of Object.keys(item)) {
|
|
324
|
+
if (!declared.has(key)) {
|
|
325
|
+
push(
|
|
326
|
+
errors,
|
|
327
|
+
`${itemPath}.${key}`,
|
|
328
|
+
owner.code,
|
|
329
|
+
`${owner.subject} declares no ${owner.noun} "${label}[].${key}"`,
|
|
330
|
+
[...declared].sort()
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
validateSettingValues(errors, itemPath, of, item, owner, `${label}[].`);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function checkLiteralParam(errors, path, containerPath, loaderId, setting, value) {
|
|
339
|
+
const expected = setting.type === "number" ? "number" : setting.type === "boolean" ? "boolean" : setting.type === "list" ? "array" : "string";
|
|
340
|
+
const actual = Array.isArray(value) ? "array" : typeof value;
|
|
341
|
+
if (actual !== expected) {
|
|
342
|
+
push(
|
|
343
|
+
errors,
|
|
344
|
+
path,
|
|
345
|
+
"invalid-value",
|
|
346
|
+
`loader "${loaderId}" param "${setting.id}" expects ${expected}, got ${actual}`
|
|
347
|
+
);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (setting.type === "select" && setting.options?.length && typeof value === "string") {
|
|
351
|
+
const allowed = setting.options.map((option) => option.value);
|
|
352
|
+
if (!allowed.includes(value)) {
|
|
353
|
+
push(errors, path, "invalid-value", `"${value}" is not a valid ${setting.id}`, allowed);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (setting.type === "entity") checkEntityRef(errors, path, value);
|
|
357
|
+
if (setting.type === "list") {
|
|
358
|
+
validateSettingValues(
|
|
359
|
+
errors,
|
|
360
|
+
containerPath,
|
|
361
|
+
[setting],
|
|
362
|
+
{ [setting.id]: value },
|
|
363
|
+
{ subject: `loader "${loaderId}"`, noun: "param", code: "unknown-loader-param" }
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function validateDataNeeds(errors, entryPath, blockType, value, declaredParams) {
|
|
368
|
+
if (!isRecord(value)) {
|
|
369
|
+
push(errors, `${entryPath}.data`, "invalid-value", "`data` maps need names to {loader, params?}");
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const blockSettings = getBlockMeta(blockType)?.settings;
|
|
373
|
+
for (const [name, need] of Object.entries(value)) {
|
|
374
|
+
const needPath = `${entryPath}.data.${name}`;
|
|
375
|
+
if (!isRecord(need) || typeof need.loader !== "string" || !need.loader) {
|
|
376
|
+
push(errors, needPath, "invalid-value", "a data need is {loader: string, params?: object}");
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (!loaderRegistry.has(need.loader)) {
|
|
380
|
+
push(errors, `${needPath}.loader`, "unknown-loader", `loader "${need.loader}" is not registered`, listLoaders());
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
for (const key of Object.keys(need)) {
|
|
384
|
+
if (!DATA_NEED_KEYS.includes(key)) {
|
|
385
|
+
push(errors, `${needPath}.${key}`, "unknown-field", `a data need has no field "${key}"`, DATA_NEED_KEYS);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (need.params === void 0) continue;
|
|
389
|
+
if (!isRecord(need.params)) {
|
|
390
|
+
push(errors, `${needPath}.params`, "invalid-value", "loader params are an object");
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const loaderSettings = loaderRegistry.getEntry(need.loader)?.meta.settings;
|
|
394
|
+
const declaredLoaderParams = loaderSettings ? collectSettingIds(loaderSettings) : null;
|
|
395
|
+
for (const [param, paramValue] of Object.entries(need.params)) {
|
|
396
|
+
const paramPath = `${needPath}.params.${param}`;
|
|
397
|
+
const setting = loaderSettings?.find((s) => s.id === param);
|
|
398
|
+
if (declaredLoaderParams && !declaredLoaderParams.has(param)) {
|
|
399
|
+
push(
|
|
400
|
+
errors,
|
|
401
|
+
paramPath,
|
|
402
|
+
"unknown-loader-param",
|
|
403
|
+
`loader "${need.loader}" declares no param "${param}"`,
|
|
404
|
+
[...declaredLoaderParams].sort()
|
|
405
|
+
);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (isLoaderParamRef(paramValue)) {
|
|
409
|
+
if (!declaredParams.includes(paramValue.$param)) {
|
|
410
|
+
push(
|
|
411
|
+
errors,
|
|
412
|
+
paramPath,
|
|
413
|
+
"unbound-route-param",
|
|
414
|
+
declaredParams.length ? `$param "${paramValue.$param}" is not a route param of this page` : `$param "${paramValue.$param}" cannot bind \u2014 this page declares no route params`,
|
|
415
|
+
declaredParams
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (isLoaderPropRef(paramValue)) {
|
|
421
|
+
const settingIds = blockSettings ? collectSettingIds(blockSettings) : null;
|
|
422
|
+
if (settingIds && !settingIds.has(paramValue.$prop)) {
|
|
423
|
+
push(
|
|
424
|
+
errors,
|
|
425
|
+
paramPath,
|
|
426
|
+
"unknown-prop-binding",
|
|
427
|
+
`$prop "${paramValue.$prop}" is not a setting of block "${blockType}"`,
|
|
428
|
+
[...settingIds].sort()
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (setting) {
|
|
434
|
+
checkLiteralParam(errors, paramPath, `${needPath}.params`, need.loader, setting, paramValue);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function validateBlockList(errors, path, value, declaredParams) {
|
|
440
|
+
if (!Array.isArray(value)) {
|
|
441
|
+
push(errors, path, "invalid-value", `expected an array of blocks, got ${typeof value}`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
value.forEach((entry, index) => {
|
|
445
|
+
const entryPath = `${path}[${index}]`;
|
|
446
|
+
if (!isRecord(entry)) {
|
|
447
|
+
push(errors, entryPath, "invalid-value", "a block entry must be an object with a `type`");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const type = entry.type;
|
|
451
|
+
if (typeof type !== "string" || !type) {
|
|
452
|
+
push(errors, `${entryPath}.type`, "invalid-value", "a block entry must declare a string `type`");
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (!blockRegistry.has(type)) {
|
|
456
|
+
push(
|
|
457
|
+
errors,
|
|
458
|
+
`${entryPath}.type`,
|
|
459
|
+
"unknown-block",
|
|
460
|
+
`block "${type}" is not registered`,
|
|
461
|
+
blockRegistry.ids().sort()
|
|
462
|
+
);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const settings = getBlockMeta(type)?.settings;
|
|
466
|
+
if (settings) {
|
|
467
|
+
const declared = collectSettingIds(settings);
|
|
468
|
+
for (const key of blockPropKeys(entry)) {
|
|
469
|
+
if (!declared.has(key)) {
|
|
470
|
+
push(
|
|
471
|
+
errors,
|
|
472
|
+
`${entryPath}.props.${key}`,
|
|
473
|
+
"unknown-setting",
|
|
474
|
+
`block "${type}" declares no setting "${key}"`,
|
|
475
|
+
[...declared].sort()
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const props = isRecord(entry.props) ? entry.props : entry;
|
|
480
|
+
validateSettingValues(errors, `${entryPath}.props`, settings, props, {
|
|
481
|
+
subject: `block "${type}"`,
|
|
482
|
+
noun: "setting",
|
|
483
|
+
code: "unknown-setting"
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
if ("data" in entry && entry.data !== void 0) {
|
|
487
|
+
validateDataNeeds(errors, entryPath, type, entry.data, declaredParams);
|
|
488
|
+
}
|
|
489
|
+
if ("children" in entry && entry.children !== void 0) {
|
|
490
|
+
validateBlockList(errors, `${entryPath}.children`, entry.children, declaredParams);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
function validateBlockTree(errors, path, value, declaredParams = []) {
|
|
495
|
+
validateBlockList(errors, path, value, declaredParams);
|
|
496
|
+
if (!Array.isArray(value)) return;
|
|
497
|
+
const nodes = toBlockNodes(value);
|
|
498
|
+
for (const violation of checkBlockConstraints(nodes)) {
|
|
499
|
+
push(errors, `${path}${violation.path}`, violation.rule, violation.detail, violation.valid);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function collectSettingIds(settings) {
|
|
503
|
+
return new Set(settings.map((setting) => setting.id));
|
|
504
|
+
}
|
|
505
|
+
var TOKEN_KEY = /^[a-zA-Z][a-zA-Z0-9-]*$/;
|
|
506
|
+
function validateTheme(errors, section) {
|
|
507
|
+
for (const field of THEME_SCALE_FIELDS) {
|
|
508
|
+
const scale = section[field];
|
|
509
|
+
if (scale === void 0) continue;
|
|
510
|
+
if (!isRecord(scale)) continue;
|
|
511
|
+
for (const [key, value] of Object.entries(scale)) {
|
|
512
|
+
const path = `theme.${field}.${key}`;
|
|
513
|
+
if (!TOKEN_KEY.test(key)) {
|
|
514
|
+
push(
|
|
515
|
+
errors,
|
|
516
|
+
path,
|
|
517
|
+
"invalid-token-key",
|
|
518
|
+
`"${key}" cannot become the CSS custom property ${THEME_SCALE_VAR_PREFIX[field]}-${key} \u2014 token keys match ${TOKEN_KEY}`
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
if (typeof value !== "string") {
|
|
522
|
+
push(errors, path, "invalid-value", `a ${field} token is a CSS value string, got ${typeof value}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function validateSectionEnvelope(errors, name, section) {
|
|
528
|
+
const spec = STORE_SECTIONS[name];
|
|
529
|
+
const version = section.version;
|
|
530
|
+
if (typeof version !== "number" || !spec.versions.includes(version)) {
|
|
531
|
+
push(
|
|
532
|
+
errors,
|
|
533
|
+
`${name}.version`,
|
|
534
|
+
"unsupported-section-version",
|
|
535
|
+
`section "${name}" declares version ${JSON.stringify(version)}, which this SDK does not understand`,
|
|
536
|
+
spec.versions.map(String)
|
|
537
|
+
);
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
const fieldNames = Object.keys(spec.fields);
|
|
541
|
+
for (const key of Object.keys(section)) {
|
|
542
|
+
if (key === "version") continue;
|
|
543
|
+
const fieldSpec = spec.fields[key];
|
|
544
|
+
if (!fieldSpec) {
|
|
545
|
+
push(errors, `${name}.${key}`, "unknown-field", `section "${name}" has no field "${key}"`, fieldNames);
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
const deprecated = fieldSpec.deprecated;
|
|
549
|
+
if (deprecated && version >= deprecated.removedAtVersion) {
|
|
550
|
+
push(
|
|
551
|
+
errors,
|
|
552
|
+
`${name}.${key}`,
|
|
553
|
+
"removed-field",
|
|
554
|
+
`field "${name}.${key}" was removed at ${name} version ${deprecated.removedAtVersion} (deprecation ${deprecated.id}); this section declares version ${version}`,
|
|
555
|
+
[deprecated.replacement]
|
|
556
|
+
);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
const value = section[key];
|
|
560
|
+
const actual = Array.isArray(value) ? "array" : typeof value;
|
|
561
|
+
if (actual !== fieldSpec.type) {
|
|
562
|
+
push(
|
|
563
|
+
errors,
|
|
564
|
+
`${name}.${key}`,
|
|
565
|
+
"invalid-value",
|
|
566
|
+
`field "${name}.${key}" expects ${fieldSpec.type}, got ${actual}`
|
|
567
|
+
);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (fieldSpec.enum && typeof value === "string" && !fieldSpec.enum.includes(value)) {
|
|
571
|
+
push(
|
|
572
|
+
errors,
|
|
573
|
+
`${name}.${key}`,
|
|
574
|
+
"invalid-value",
|
|
575
|
+
`"${value}" is not a valid ${name}.${key}`,
|
|
576
|
+
fieldSpec.enum
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
for (const [field, fieldSpec] of Object.entries(spec.fields)) {
|
|
581
|
+
if (fieldSpec.required && section[field] === void 0) {
|
|
582
|
+
push(errors, `${name}.${field}`, "missing-field", `section "${name}" requires "${field}"`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
function validateCommerce(errors, section) {
|
|
588
|
+
const payments = section.payments;
|
|
589
|
+
if (isRecord(payments)) {
|
|
590
|
+
if (payments.mode !== void 0) {
|
|
591
|
+
if (typeof payments.mode !== "string" || !STORE_PAYMENT_MODES.includes(payments.mode)) {
|
|
592
|
+
push(
|
|
593
|
+
errors,
|
|
594
|
+
"commerce.payments.mode",
|
|
595
|
+
"invalid-value",
|
|
596
|
+
`"${String(payments.mode)}" is not a payment mode`,
|
|
597
|
+
STORE_PAYMENT_MODES
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (payments.methods !== void 0) {
|
|
602
|
+
if (!Array.isArray(payments.methods)) {
|
|
603
|
+
push(errors, "commerce.payments.methods", "invalid-value", "payments.methods must be an array");
|
|
604
|
+
} else {
|
|
605
|
+
payments.methods.forEach((method, index) => {
|
|
606
|
+
if (typeof method !== "string" || !STORE_PAYMENT_METHOD_KINDS.includes(method)) {
|
|
607
|
+
push(
|
|
608
|
+
errors,
|
|
609
|
+
`commerce.payments.methods[${index}]`,
|
|
610
|
+
"invalid-value",
|
|
611
|
+
`"${String(method)}" is not a payment method`,
|
|
612
|
+
STORE_PAYMENT_METHOD_KINDS
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const discounts = section.discounts;
|
|
620
|
+
if (Array.isArray(discounts)) {
|
|
621
|
+
discounts.forEach((discount, index) => {
|
|
622
|
+
if (!isRecord(discount) || typeof discount.code !== "string" || typeof discount.percent !== "number") {
|
|
623
|
+
push(
|
|
624
|
+
errors,
|
|
625
|
+
`commerce.discounts[${index}]`,
|
|
626
|
+
"invalid-value",
|
|
627
|
+
"a discount is {code: string, percent: number, title?: string}"
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
var CONTENT_SLUG = /^[a-z0-9][a-z0-9-]*$/;
|
|
634
|
+
function validateContentPages(errors, basePath, value) {
|
|
635
|
+
if (!isRecord(value)) {
|
|
636
|
+
push(errors, basePath, "invalid-value", "pages.content maps slugs to {title?, path?, blocks: [...]}");
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const pathOwners = /* @__PURE__ */ new Map();
|
|
640
|
+
for (const [slug, page] of Object.entries(value)) {
|
|
641
|
+
const pagePath = `${basePath}.${slug}`;
|
|
642
|
+
if (!CONTENT_SLUG.test(slug)) {
|
|
643
|
+
push(
|
|
644
|
+
errors,
|
|
645
|
+
pagePath,
|
|
646
|
+
"invalid-value",
|
|
647
|
+
`"${slug}" cannot become the route "/${slug}" \u2014 content slugs match ${CONTENT_SLUG}`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (!isRecord(page) || page.blocks === void 0) {
|
|
651
|
+
push(errors, pagePath, "invalid-value", "a content page is {title?, path?, blocks: [...]}");
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (page.title !== void 0 && typeof page.title !== "string") {
|
|
655
|
+
push(errors, `${pagePath}.title`, "invalid-value", `title expects string, got ${typeof page.title}`);
|
|
656
|
+
}
|
|
657
|
+
let routePath = `/${slug}`;
|
|
658
|
+
if (page.path !== void 0) {
|
|
659
|
+
if (typeof page.path !== "string" || !page.path.startsWith("/")) {
|
|
660
|
+
push(errors, `${pagePath}.path`, "invalid-value", `path is an absolute route ('/${slug}'), got ${JSON.stringify(page.path)}`);
|
|
661
|
+
} else {
|
|
662
|
+
routePath = page.path;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const owner = pathOwners.get(routePath);
|
|
666
|
+
if (owner) {
|
|
667
|
+
push(
|
|
668
|
+
errors,
|
|
669
|
+
`${pagePath}.path`,
|
|
670
|
+
"duplicate-content-path",
|
|
671
|
+
`content pages "${owner}" and "${slug}" both resolve to "${routePath}" \u2014 one of them can never render`
|
|
672
|
+
);
|
|
673
|
+
} else {
|
|
674
|
+
pathOwners.set(routePath, slug);
|
|
675
|
+
}
|
|
676
|
+
validateBlockTree(errors, `${pagePath}.blocks`, page.blocks, routePathParams(routePath));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function validatePresentation(errors, section) {
|
|
680
|
+
const home = section.home;
|
|
681
|
+
if (isRecord(home) && home.sections !== void 0) {
|
|
682
|
+
validateBlockTree(errors, "presentation.home.sections", home.sections);
|
|
683
|
+
}
|
|
684
|
+
const pages = section.pages;
|
|
685
|
+
if (isRecord(pages)) {
|
|
686
|
+
const validKinds = [...STOREFRONT_PAGE_KINDS];
|
|
687
|
+
for (const [kind, page] of Object.entries(pages)) {
|
|
688
|
+
const pagePath = `presentation.pages.${kind}`;
|
|
689
|
+
if (!validKinds.includes(kind)) {
|
|
690
|
+
push(errors, pagePath, "unknown-page-kind", `"${kind}" is not a composable page kind`, validKinds);
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (kind === "content") {
|
|
694
|
+
validateContentPages(errors, pagePath, page);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (!isRecord(page) || page.blocks === void 0) {
|
|
698
|
+
push(errors, pagePath, "invalid-value", "a composed page is {blocks: [...]}");
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
validateBlockTree(errors, `${pagePath}.blocks`, page.blocks, PAGE_KIND_PARAMS[kind] ?? []);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const slots = section.slots;
|
|
705
|
+
if (isRecord(slots)) {
|
|
706
|
+
for (const [area, areaSlots] of Object.entries(slots)) {
|
|
707
|
+
const areaPath = `presentation.slots.${area}`;
|
|
708
|
+
if (!SLOT_AREAS.includes(area)) {
|
|
709
|
+
push(errors, areaPath, "unknown-slot-area", `"${area}" is not a slot area`, SLOT_AREAS);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (!isRecord(areaSlots)) {
|
|
713
|
+
push(errors, areaPath, "invalid-value", "a slot area maps slot names to block lists");
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const validNames = slotNamesFor(area);
|
|
717
|
+
for (const [slotName, blocks] of Object.entries(areaSlots)) {
|
|
718
|
+
const slotPath = `${areaPath}.${slotName}`;
|
|
719
|
+
if (!validNames.includes(slotName)) {
|
|
720
|
+
push(errors, slotPath, "unknown-slot", `"${area}.${slotName}" is not a slot`, validNames);
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
validateBlockTree(errors, slotPath, blocks, SLOT_AREA_PARAMS[area] ?? []);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
var ENTITY_KEY_RE = /^[a-z][a-z0-9_]*$/;
|
|
729
|
+
var ENTITY_FIELD_RE = /^[a-z][a-z0-9_]*$/;
|
|
730
|
+
function validateEntitiesSection(errors, section) {
|
|
731
|
+
const definitions = section.definitions;
|
|
732
|
+
if (definitions === void 0 || !isRecord(definitions)) return;
|
|
733
|
+
for (const [key, declaration] of Object.entries(definitions)) {
|
|
734
|
+
const path = `entities.definitions.${key}`;
|
|
735
|
+
if (!ENTITY_KEY_RE.test(key)) {
|
|
736
|
+
push(errors, path, "invalid-value", `"${key}" is not a valid entity key \u2014 keys are snake_case`);
|
|
737
|
+
}
|
|
738
|
+
if (!isRecord(declaration) || !isRecord(declaration.fields) || Object.keys(declaration.fields).length === 0) {
|
|
739
|
+
push(errors, path, "invalid-value", "an entity declaration is {fields: {<name>: {type, \u2026}}, scope?, access?, indexes?}");
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const fields = declaration.fields;
|
|
743
|
+
const fieldNames = Object.keys(fields);
|
|
744
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
745
|
+
const fieldPath = `${path}.fields.${name}`;
|
|
746
|
+
if (!ENTITY_FIELD_RE.test(name)) {
|
|
747
|
+
push(errors, fieldPath, "invalid-value", `"${name}" is not a valid field name \u2014 fields are snake_case`);
|
|
748
|
+
}
|
|
749
|
+
if (RESERVED_ENTITY_FIELDS.includes(name)) {
|
|
750
|
+
push(errors, fieldPath, "invalid-value", `"${name}" is a structural record column`, RESERVED_ENTITY_FIELDS);
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (!isRecord(field) || typeof field.type !== "string") {
|
|
754
|
+
push(errors, fieldPath, "invalid-value", "a field is {type: <entity field type>, label?, required?, \u2026}");
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (!ENTITY_FIELD_TYPES.includes(field.type)) {
|
|
758
|
+
push(errors, `${fieldPath}.type`, "unknown-entity-field-type", `"${field.type}" is not an entity field type`, ENTITY_FIELD_TYPES);
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (field.type === "ref") {
|
|
762
|
+
if (typeof field.entity !== "string" || !field.entity) {
|
|
763
|
+
push(errors, fieldPath, "invalid-value", `a ref field names its target: {type: 'ref', entity: '<key>'}`);
|
|
764
|
+
} else {
|
|
765
|
+
checkEntityRef(errors, `${fieldPath}.entity`, field.entity);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const scope = declaration.scope;
|
|
770
|
+
if (scope !== void 0 && scope !== "tenant") {
|
|
771
|
+
push(errors, `${path}.scope`, "invalid-value", `"${String(scope)}" is not an entity scope`, ["tenant"]);
|
|
772
|
+
}
|
|
773
|
+
const access = declaration.access;
|
|
774
|
+
if (access !== void 0) {
|
|
775
|
+
if (!isRecord(access)) {
|
|
776
|
+
push(errors, `${path}.access`, "invalid-value", "access is {read: 'public'|'staff', write: 'staff'}");
|
|
777
|
+
} else {
|
|
778
|
+
if (access.read !== void 0 && access.read !== "public" && access.read !== "staff") {
|
|
779
|
+
push(errors, `${path}.access.read`, "invalid-value", `"${String(access.read)}" is not a read access level`, ["public", "staff"]);
|
|
780
|
+
}
|
|
781
|
+
if (access.write !== void 0 && access.write !== "staff") {
|
|
782
|
+
push(errors, `${path}.access.write`, "invalid-value", `"${String(access.write)}" is not a write access level \u2014 shopper writes go through actions (M7), not RLS`, ["staff"]);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const references = (where, name) => {
|
|
787
|
+
if (typeof name !== "string" || !fieldNames.includes(name)) {
|
|
788
|
+
push(errors, `${path}.${where}`, "invalid-value", `${where} references undeclared field "${String(name)}"`, fieldNames);
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
if (Array.isArray(declaration.indexes)) {
|
|
792
|
+
declaration.indexes.forEach((tuple, i) => {
|
|
793
|
+
if (!Array.isArray(tuple)) {
|
|
794
|
+
push(errors, `${path}.indexes[${i}]`, "invalid-value", "an index is an array of declared field names");
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
for (const name of tuple) references(`indexes[${i}]`, name);
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
if (declaration.titleField !== void 0) references("titleField", declaration.titleField);
|
|
801
|
+
if (Array.isArray(declaration.displayFields)) {
|
|
802
|
+
for (const name of declaration.displayFields) references("displayFields", name);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function validatePromotionRule(errors, path, rule) {
|
|
807
|
+
if (!isRecord(rule) || typeof rule.attribute !== "string" || typeof rule.operator !== "string") {
|
|
808
|
+
push(errors, path, "invalid-value", "a rule is {attribute, operator, values: [...]}");
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
if (!hasRuleAttribute(rule.attribute)) {
|
|
812
|
+
push(
|
|
813
|
+
errors,
|
|
814
|
+
`${path}.attribute`,
|
|
815
|
+
"unknown-rule-attribute",
|
|
816
|
+
`"${rule.attribute}" is not in the rule attribute vocabulary`,
|
|
817
|
+
listRuleAttributeKeys()
|
|
818
|
+
);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (!RULE_OPERATORS.includes(rule.operator)) {
|
|
822
|
+
push(errors, `${path}.operator`, "unknown-rule-operator", `"${rule.operator}" is not a rule operator`, RULE_OPERATORS);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const attribute = resolveRuleAttribute(rule.attribute);
|
|
826
|
+
if (!attribute.operators.includes(rule.operator)) {
|
|
827
|
+
push(
|
|
828
|
+
errors,
|
|
829
|
+
`${path}.operator`,
|
|
830
|
+
"unsupported-rule-operator",
|
|
831
|
+
`attribute "${rule.attribute}" does not support operator "${rule.operator}"`,
|
|
832
|
+
attribute.operators
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
if (!Array.isArray(rule.values) || rule.values.length === 0) {
|
|
836
|
+
push(errors, `${path}.values`, "invalid-value", "rule values are a non-empty array of scalars");
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
rule.values.forEach((value, index) => {
|
|
840
|
+
const t = typeof value;
|
|
841
|
+
if (t !== "string" && t !== "number" && t !== "boolean") {
|
|
842
|
+
push(errors, `${path}.values[${index}]`, "invalid-value", `a rule value is a JSON scalar, got ${t}`);
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
function validateRuleList(errors, path, value) {
|
|
847
|
+
if (value === void 0) return;
|
|
848
|
+
if (!Array.isArray(value)) {
|
|
849
|
+
push(errors, path, "invalid-value", `expected an array of rules, got ${typeof value}`);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
value.forEach((rule, index) => validatePromotionRule(errors, `${path}[${index}]`, rule));
|
|
853
|
+
}
|
|
854
|
+
function validatePromotionsSection(errors, section) {
|
|
855
|
+
const promotions = section.promotions;
|
|
856
|
+
if (promotions === void 0 || !Array.isArray(promotions)) return;
|
|
857
|
+
promotions.forEach((promotion, index) => {
|
|
858
|
+
const path = `promotions.promotions[${index}]`;
|
|
859
|
+
if (!isRecord(promotion) || typeof promotion.id !== "string" || !promotion.id) {
|
|
860
|
+
push(errors, path, "invalid-value", "a promotion is {id, code?, isAutomatic?, rules?, applicationMethod}");
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (promotion.type !== void 0 && !PROMOTION_TYPES.includes(promotion.type)) {
|
|
864
|
+
push(errors, `${path}.type`, "invalid-value", `"${String(promotion.type)}" is not a promotion type`, PROMOTION_TYPES);
|
|
865
|
+
}
|
|
866
|
+
if (promotion.status !== void 0 && !PROMOTION_STATUSES.includes(promotion.status)) {
|
|
867
|
+
push(errors, `${path}.status`, "invalid-value", `"${String(promotion.status)}" is not a promotion status`, PROMOTION_STATUSES);
|
|
868
|
+
}
|
|
869
|
+
if (promotion.code === void 0 && promotion.isAutomatic !== true) {
|
|
870
|
+
push(
|
|
871
|
+
errors,
|
|
872
|
+
`${path}.code`,
|
|
873
|
+
"invalid-value",
|
|
874
|
+
"a promotion needs a code or isAutomatic: true \u2014 otherwise nothing can ever trigger it"
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
validateRuleList(errors, `${path}.rules`, promotion.rules);
|
|
878
|
+
const method = promotion.applicationMethod;
|
|
879
|
+
if (!isRecord(method)) {
|
|
880
|
+
push(errors, `${path}.applicationMethod`, "invalid-value", "a promotion carries an applicationMethod ({type, value, targetType, \u2026})");
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const methodPath = `${path}.applicationMethod`;
|
|
884
|
+
if (typeof method.type !== "string" || !APPLICATION_METHOD_TYPES.includes(method.type)) {
|
|
885
|
+
push(errors, `${methodPath}.type`, "invalid-value", `"${String(method.type)}" is not an application method type`, APPLICATION_METHOD_TYPES);
|
|
886
|
+
}
|
|
887
|
+
if (typeof method.value !== "number" || method.value < 0) {
|
|
888
|
+
push(errors, `${methodPath}.value`, "invalid-value", "applicationMethod.value is a non-negative number");
|
|
889
|
+
}
|
|
890
|
+
if (typeof method.targetType !== "string" || !APPLICATION_METHOD_TARGET_TYPES.includes(method.targetType)) {
|
|
891
|
+
push(errors, `${methodPath}.targetType`, "invalid-value", `"${String(method.targetType)}" is not a target type`, APPLICATION_METHOD_TARGET_TYPES);
|
|
892
|
+
}
|
|
893
|
+
if (method.allocation !== void 0 && !APPLICATION_METHOD_ALLOCATIONS.includes(method.allocation)) {
|
|
894
|
+
push(errors, `${methodPath}.allocation`, "invalid-value", `"${String(method.allocation)}" is not an allocation`, APPLICATION_METHOD_ALLOCATIONS);
|
|
895
|
+
}
|
|
896
|
+
for (const field of ["maxQuantity", "applyToQuantity", "buyRulesMinQuantity"]) {
|
|
897
|
+
const value = method[field];
|
|
898
|
+
if (value !== void 0 && value !== null && (typeof value !== "number" || value < 1)) {
|
|
899
|
+
push(errors, `${methodPath}.${field}`, "invalid-value", `${field} is a positive integer when set`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
validateRuleList(errors, `${methodPath}.targetRules`, method.targetRules);
|
|
903
|
+
validateRuleList(errors, `${methodPath}.buyRules`, method.buyRules);
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
function validateHandleRef(errors, path, name) {
|
|
907
|
+
if (typeof name !== "string" || !name) {
|
|
908
|
+
push(errors, path, "invalid-value", "a code reference is a registered handle name (string)");
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (!hasHandle(name)) {
|
|
912
|
+
push(errors, path, "unknown-handle", `handle "${name}" is not registered`, listHandles());
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function validateCode(errors, section) {
|
|
916
|
+
for (const field of ["logo", "provider", "auth", "footerCredit"]) {
|
|
917
|
+
if (section[field] !== void 0) validateHandleRef(errors, `code.${field}`, section[field]);
|
|
918
|
+
}
|
|
919
|
+
const components = section.components;
|
|
920
|
+
if (isRecord(components)) {
|
|
921
|
+
for (const [slot, handle] of Object.entries(components)) {
|
|
922
|
+
const slotPath = `code.components.${slot}`;
|
|
923
|
+
if (!STOREFRONT_COMPONENT_KEYS.includes(slot)) {
|
|
924
|
+
push(
|
|
925
|
+
errors,
|
|
926
|
+
slotPath,
|
|
927
|
+
"unknown-component-slot",
|
|
928
|
+
`"${slot}" is not an overridable storefront component`,
|
|
929
|
+
STOREFRONT_COMPONENT_KEYS
|
|
930
|
+
);
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
validateHandleRef(errors, slotPath, handle);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const routes = section.routes;
|
|
937
|
+
if (Array.isArray(routes)) {
|
|
938
|
+
routes.forEach((route, index) => {
|
|
939
|
+
const routePath = `code.routes[${index}]`;
|
|
940
|
+
if (!isRecord(route)) {
|
|
941
|
+
push(errors, routePath, "invalid-value", "a route is {id, path, kind?, chrome?, component}");
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
for (const required of ["id", "path"]) {
|
|
945
|
+
if (typeof route[required] !== "string" || !route[required]) {
|
|
946
|
+
push(errors, `${routePath}.${required}`, "missing-field", `route requires a string "${required}"`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
for (const key of Object.keys(route)) {
|
|
950
|
+
if (!CODE_ROUTE_KEYS.includes(key)) {
|
|
951
|
+
push(errors, `${routePath}.${key}`, "unknown-field", `a route has no field "${key}"`, CODE_ROUTE_KEYS);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (route.kind !== void 0 && !STORE_ROUTE_KINDS.includes(route.kind)) {
|
|
955
|
+
push(
|
|
956
|
+
errors,
|
|
957
|
+
`${routePath}.kind`,
|
|
958
|
+
"invalid-value",
|
|
959
|
+
`"${String(route.kind)}" is not a route kind`,
|
|
960
|
+
STORE_ROUTE_KINDS
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
if (route.chrome !== void 0 && !STORE_ROUTE_CHROMES.includes(route.chrome)) {
|
|
964
|
+
push(
|
|
965
|
+
errors,
|
|
966
|
+
`${routePath}.chrome`,
|
|
967
|
+
"invalid-value",
|
|
968
|
+
`"${String(route.chrome)}" is not a route chrome`,
|
|
969
|
+
STORE_ROUTE_CHROMES
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
validateHandleRef(errors, `${routePath}.component`, route.component);
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function validateStore(doc) {
|
|
977
|
+
registerStorefrontBlocks();
|
|
978
|
+
const errors = [];
|
|
979
|
+
knownEntities = new Set(listEntityDeclarations());
|
|
980
|
+
if (isRecord(doc)) {
|
|
981
|
+
const entitiesSection = doc.entities;
|
|
982
|
+
if (isRecord(entitiesSection) && isRecord(entitiesSection.definitions)) {
|
|
983
|
+
for (const key of Object.keys(entitiesSection.definitions)) knownEntities.add(key);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if (!isRecord(doc)) {
|
|
987
|
+
push(errors, "", "invalid-document", `a store document is an object, got ${doc === null ? "null" : typeof doc}`);
|
|
988
|
+
return { ok: false, errors };
|
|
989
|
+
}
|
|
990
|
+
if (doc.$kind !== STORE_DOCUMENT_KIND) {
|
|
991
|
+
push(
|
|
992
|
+
errors,
|
|
993
|
+
"$kind",
|
|
994
|
+
"invalid-document",
|
|
995
|
+
`expected $kind "${STORE_DOCUMENT_KIND}", got ${JSON.stringify(doc.$kind)}`,
|
|
996
|
+
[STORE_DOCUMENT_KIND]
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
if (doc.store === void 0) {
|
|
1000
|
+
push(errors, "store", "missing-section", 'the "store" section (name/currency/locale) is required', [
|
|
1001
|
+
...STORE_SECTION_NAMES
|
|
1002
|
+
]);
|
|
1003
|
+
}
|
|
1004
|
+
for (const [name, section] of Object.entries(doc)) {
|
|
1005
|
+
if (name === "$kind") continue;
|
|
1006
|
+
if (!STORE_SECTION_NAMES.includes(name)) {
|
|
1007
|
+
push(errors, name, "unknown-section", `"${name}" is not a document section`, STORE_SECTION_NAMES);
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (!isRecord(section)) {
|
|
1011
|
+
push(errors, name, "invalid-value", `section "${name}" must be an object with a version`);
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
const sectionName = name;
|
|
1015
|
+
if (!validateSectionEnvelope(errors, sectionName, section)) continue;
|
|
1016
|
+
if (sectionName === "theme") validateTheme(errors, section);
|
|
1017
|
+
if (sectionName === "entities") validateEntitiesSection(errors, section);
|
|
1018
|
+
if (sectionName === "promotions") validatePromotionsSection(errors, section);
|
|
1019
|
+
if (sectionName === "commerce") validateCommerce(errors, section);
|
|
1020
|
+
if (sectionName === "presentation") validatePresentation(errors, section);
|
|
1021
|
+
if (sectionName === "code") validateCode(errors, section);
|
|
1022
|
+
}
|
|
1023
|
+
return { ok: errors.length === 0, errors };
|
|
1024
|
+
}
|
|
1025
|
+
function slugifyStoreName(name) {
|
|
1026
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "app";
|
|
1027
|
+
}
|
|
1028
|
+
function codeRef(value, configPath, handleSuffix, ctx) {
|
|
1029
|
+
const existing = handleNameFor(value);
|
|
1030
|
+
if (existing) return existing;
|
|
1031
|
+
const name = `${ctx.appId}.${handleSuffix}`;
|
|
1032
|
+
if (!ctx.autoRegister) {
|
|
1033
|
+
throw new Error(
|
|
1034
|
+
`exportStore: config.${configPath} holds code that is not a registered handle. Call registerHandle('${name}', value) before exporting, or export with { registerMissingHandles: true } (defineStorefront does).`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
registerHandle(
|
|
1038
|
+
name,
|
|
1039
|
+
value,
|
|
1040
|
+
{ description: `Auto-registered from config.${configPath}`, source: "app" },
|
|
1041
|
+
// The app re-declaring its config (module re-evaluation, HMR) re-derives
|
|
1042
|
+
// the same name for a fresh value; replacing is the correct semantics.
|
|
1043
|
+
{ replace: true }
|
|
1044
|
+
);
|
|
1045
|
+
return name;
|
|
1046
|
+
}
|
|
1047
|
+
function findNonJsonPath(value, path) {
|
|
1048
|
+
if (value === null || value === void 0) return null;
|
|
1049
|
+
const t = typeof value;
|
|
1050
|
+
if (t === "string" || t === "number" || t === "boolean") return null;
|
|
1051
|
+
if (Array.isArray(value)) {
|
|
1052
|
+
for (let i = 0; i < value.length; i++) {
|
|
1053
|
+
const found = findNonJsonPath(value[i], `${path}[${i}]`);
|
|
1054
|
+
if (found) return found;
|
|
1055
|
+
}
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
if (t === "object") {
|
|
1059
|
+
const record = value;
|
|
1060
|
+
if ("$$typeof" in record) return path;
|
|
1061
|
+
const proto = Object.getPrototypeOf(record);
|
|
1062
|
+
if (proto !== Object.prototype && proto !== null) return path;
|
|
1063
|
+
for (const [key, child] of Object.entries(record)) {
|
|
1064
|
+
const found = findNonJsonPath(child, `${path}.${key}`);
|
|
1065
|
+
if (found) return found;
|
|
1066
|
+
}
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
return path;
|
|
1070
|
+
}
|
|
1071
|
+
function hasContent(section) {
|
|
1072
|
+
return Object.keys(section).some((key) => key !== "version");
|
|
1073
|
+
}
|
|
1074
|
+
function exportStore(config, options = {}) {
|
|
1075
|
+
const ctx = {
|
|
1076
|
+
appId: options.appId ?? slugifyStoreName(config.name),
|
|
1077
|
+
autoRegister: options.registerMissingHandles ?? false
|
|
1078
|
+
};
|
|
1079
|
+
const code = { version: 1 };
|
|
1080
|
+
let presentationLogo;
|
|
1081
|
+
if (config.logo !== void 0) {
|
|
1082
|
+
if (typeof config.logo === "string") presentationLogo = config.logo;
|
|
1083
|
+
else code.logo = codeRef(config.logo, "logo", "logo", ctx);
|
|
1084
|
+
}
|
|
1085
|
+
let footerData;
|
|
1086
|
+
if (config.footer !== void 0) {
|
|
1087
|
+
const { credit, ...rest } = config.footer;
|
|
1088
|
+
if (credit === void 0 || typeof credit === "string") {
|
|
1089
|
+
footerData = config.footer;
|
|
1090
|
+
} else {
|
|
1091
|
+
footerData = rest;
|
|
1092
|
+
code.footerCredit = codeRef(credit, "footer.credit", "footer-credit", ctx);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
let searchData;
|
|
1096
|
+
if (config.search !== void 0) {
|
|
1097
|
+
const { renderSuggestion, ...rest } = config.search;
|
|
1098
|
+
searchData = rest;
|
|
1099
|
+
if (renderSuggestion !== void 0) {
|
|
1100
|
+
code.searchRenderSuggestion = codeRef(
|
|
1101
|
+
renderSuggestion,
|
|
1102
|
+
"search.renderSuggestion",
|
|
1103
|
+
"search-render-suggestion",
|
|
1104
|
+
ctx
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
let authData;
|
|
1109
|
+
if (config.auth !== void 0) {
|
|
1110
|
+
if (isJsonValue(config.auth)) authData = config.auth;
|
|
1111
|
+
else code.auth = codeRef(config.auth, "auth", "auth", ctx);
|
|
1112
|
+
}
|
|
1113
|
+
if (config.provider !== void 0) {
|
|
1114
|
+
code.provider = codeRef(config.provider, "provider", "provider", ctx);
|
|
1115
|
+
}
|
|
1116
|
+
if (config.components !== void 0) {
|
|
1117
|
+
const componentRefs = {};
|
|
1118
|
+
for (const [key, override] of Object.entries(config.components)) {
|
|
1119
|
+
if (override === void 0) continue;
|
|
1120
|
+
componentRefs[key] = codeRef(override, `components.${key}`, `components.${key}`, ctx);
|
|
1121
|
+
}
|
|
1122
|
+
if (Object.keys(componentRefs).length > 0) code.components = componentRefs;
|
|
1123
|
+
}
|
|
1124
|
+
if (config.routes !== void 0 && config.routes.length > 0) {
|
|
1125
|
+
code.routes = config.routes.map(
|
|
1126
|
+
(route) => compact({
|
|
1127
|
+
id: route.id,
|
|
1128
|
+
path: route.path,
|
|
1129
|
+
kind: route.kind,
|
|
1130
|
+
chrome: route.chrome,
|
|
1131
|
+
component: codeRef(route.component, `routes[${route.id}].component`, `routes.${route.id}`, ctx)
|
|
1132
|
+
})
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
const sections = {
|
|
1136
|
+
store: compact({
|
|
1137
|
+
version: 1,
|
|
1138
|
+
name: config.name,
|
|
1139
|
+
currency: config.currency,
|
|
1140
|
+
locale: config.locale
|
|
1141
|
+
}),
|
|
1142
|
+
theme: config.theme ? { version: 1, ...compact(config.theme) } : { version: 1 },
|
|
1143
|
+
commerce: compact({
|
|
1144
|
+
version: 1,
|
|
1145
|
+
commerceMode: config.commerceMode,
|
|
1146
|
+
shipping: config.shipping,
|
|
1147
|
+
payments: config.payments,
|
|
1148
|
+
discounts: config.discounts,
|
|
1149
|
+
enquiry: config.enquiry,
|
|
1150
|
+
features: config.features
|
|
1151
|
+
}),
|
|
1152
|
+
presentation: compact({
|
|
1153
|
+
version: 1,
|
|
1154
|
+
announcement: config.announcement,
|
|
1155
|
+
logo: presentationLogo,
|
|
1156
|
+
home: config.home,
|
|
1157
|
+
pages: config.pages,
|
|
1158
|
+
slots: config.slots,
|
|
1159
|
+
nav: config.nav,
|
|
1160
|
+
footer: footerData,
|
|
1161
|
+
imageLoading: config.imageLoading,
|
|
1162
|
+
catalogFacets: config.catalogFacets,
|
|
1163
|
+
productSpecs: config.productSpecs,
|
|
1164
|
+
search: searchData
|
|
1165
|
+
}),
|
|
1166
|
+
content: compact({ version: 1, legal: config.legal, reviews: config.reviews }),
|
|
1167
|
+
catalog: config.catalog ? { version: 1, ...compact(config.catalog) } : { version: 1 },
|
|
1168
|
+
entities: config.entities ? { version: 1, definitions: config.entities } : { version: 1 },
|
|
1169
|
+
promotions: config.promotions ? { version: 1, promotions: config.promotions } : { version: 1 },
|
|
1170
|
+
backend: compact({
|
|
1171
|
+
version: 1,
|
|
1172
|
+
provider: config.backend?.provider,
|
|
1173
|
+
url: config.backend?.url,
|
|
1174
|
+
supabaseUrl: config.supabaseUrl,
|
|
1175
|
+
supabaseAnonKey: config.supabaseAnonKey
|
|
1176
|
+
}),
|
|
1177
|
+
auth: authData ? { version: 1, ...compact(authData) } : { version: 1 },
|
|
1178
|
+
code: compact(code)
|
|
1179
|
+
};
|
|
1180
|
+
const include = {
|
|
1181
|
+
theme: config.theme !== void 0,
|
|
1182
|
+
catalog: config.catalog !== void 0,
|
|
1183
|
+
entities: config.entities !== void 0,
|
|
1184
|
+
promotions: config.promotions !== void 0,
|
|
1185
|
+
auth: authData !== void 0,
|
|
1186
|
+
commerce: hasContent(sections.commerce),
|
|
1187
|
+
presentation: hasContent(sections.presentation),
|
|
1188
|
+
content: hasContent(sections.content),
|
|
1189
|
+
backend: hasContent(sections.backend),
|
|
1190
|
+
code: hasContent(sections.code)
|
|
1191
|
+
};
|
|
1192
|
+
const doc = { $kind: STORE_DOCUMENT_KIND, store: sections.store };
|
|
1193
|
+
for (const name of ["theme", "commerce", "presentation", "content", "catalog", "entities", "promotions", "backend", "auth", "code"]) {
|
|
1194
|
+
if (include[name]) doc[name] = sections[name];
|
|
1195
|
+
}
|
|
1196
|
+
for (const [name, section] of Object.entries(doc)) {
|
|
1197
|
+
if (name === "$kind") continue;
|
|
1198
|
+
const offending = findNonJsonPath(section, name);
|
|
1199
|
+
if (offending) {
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
`exportStore: ${offending} holds a non-serializable value. Code belongs in a registered handle (registerHandle) referenced from the document's code section \u2014 data sections must be pure JSON.`
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return doc;
|
|
1206
|
+
}
|
|
1207
|
+
function formatErrors(errors) {
|
|
1208
|
+
return errors.map((e) => ` - ${e.path || "(document)"}: ${e.message}${e.valid ? ` (valid: ${e.valid.join(", ")})` : ""}`).join("\n");
|
|
1209
|
+
}
|
|
1210
|
+
function hydrateStoreDocument(input, options = {}) {
|
|
1211
|
+
const onError = options.onError ?? "throw";
|
|
1212
|
+
const report = validateStore(input);
|
|
1213
|
+
if (!report.ok) {
|
|
1214
|
+
const message = `Store document failed validation:
|
|
1215
|
+
${formatErrors(report.errors)}`;
|
|
1216
|
+
if (onError === "throw") throw new Error(message);
|
|
1217
|
+
console.error(`[storefront] ${message}`);
|
|
1218
|
+
}
|
|
1219
|
+
const doc = input;
|
|
1220
|
+
const resolve = (name, path) => {
|
|
1221
|
+
try {
|
|
1222
|
+
return resolveHandle(name);
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
if (onError === "throw") throw err;
|
|
1225
|
+
console.error(`[storefront] dropping ${path}: ${errorMessage(err)}`);
|
|
1226
|
+
return void 0;
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
const config = { name: doc.store?.name ?? "" };
|
|
1230
|
+
const set = (key, value) => {
|
|
1231
|
+
if (value !== void 0) config[key] = value;
|
|
1232
|
+
};
|
|
1233
|
+
set("currency", doc.store?.currency);
|
|
1234
|
+
set("locale", doc.store?.locale);
|
|
1235
|
+
if (doc.theme) {
|
|
1236
|
+
const { version: _v, ...theme } = doc.theme;
|
|
1237
|
+
set("theme", theme);
|
|
1238
|
+
}
|
|
1239
|
+
if (doc.commerce) {
|
|
1240
|
+
set("commerceMode", doc.commerce.commerceMode);
|
|
1241
|
+
set("shipping", doc.commerce.shipping);
|
|
1242
|
+
set("payments", doc.commerce.payments);
|
|
1243
|
+
set("discounts", doc.commerce.discounts);
|
|
1244
|
+
set("enquiry", doc.commerce.enquiry);
|
|
1245
|
+
set("features", doc.commerce.features);
|
|
1246
|
+
}
|
|
1247
|
+
if (doc.presentation) {
|
|
1248
|
+
set("announcement", doc.presentation.announcement);
|
|
1249
|
+
set("home", doc.presentation.home);
|
|
1250
|
+
set("pages", doc.presentation.pages);
|
|
1251
|
+
set("slots", doc.presentation.slots);
|
|
1252
|
+
set("nav", doc.presentation.nav);
|
|
1253
|
+
set("imageLoading", doc.presentation.imageLoading);
|
|
1254
|
+
set("catalogFacets", doc.presentation.catalogFacets);
|
|
1255
|
+
set("productSpecs", doc.presentation.productSpecs);
|
|
1256
|
+
set("search", doc.presentation.search);
|
|
1257
|
+
if (doc.presentation.logo !== void 0) set("logo", doc.presentation.logo);
|
|
1258
|
+
}
|
|
1259
|
+
if (doc.content) {
|
|
1260
|
+
set("legal", doc.content.legal);
|
|
1261
|
+
set("reviews", doc.content.reviews);
|
|
1262
|
+
}
|
|
1263
|
+
if (doc.catalog) {
|
|
1264
|
+
const { version: _v, ...seed } = doc.catalog;
|
|
1265
|
+
set("catalog", seed);
|
|
1266
|
+
}
|
|
1267
|
+
if (doc.entities?.definitions) set("entities", doc.entities.definitions);
|
|
1268
|
+
if (doc.promotions?.promotions) set("promotions", doc.promotions.promotions);
|
|
1269
|
+
if (doc.backend) {
|
|
1270
|
+
const { provider, url, supabaseUrl, supabaseAnonKey } = doc.backend;
|
|
1271
|
+
if (provider !== void 0 || url !== void 0) set("backend", compact({ provider, url }));
|
|
1272
|
+
set("supabaseUrl", supabaseUrl);
|
|
1273
|
+
set("supabaseAnonKey", supabaseAnonKey);
|
|
1274
|
+
}
|
|
1275
|
+
if (doc.auth) {
|
|
1276
|
+
const { version: _v, ...auth } = doc.auth;
|
|
1277
|
+
set("auth", auth);
|
|
1278
|
+
}
|
|
1279
|
+
const code = doc.code;
|
|
1280
|
+
if (code) {
|
|
1281
|
+
if (code.logo) set("logo", resolve(code.logo, "code.logo"));
|
|
1282
|
+
if (code.provider) set("provider", resolve(code.provider, "code.provider"));
|
|
1283
|
+
if (code.auth) set("auth", resolve(code.auth, "code.auth"));
|
|
1284
|
+
if (code.components) {
|
|
1285
|
+
const components = {};
|
|
1286
|
+
for (const [key, handle] of Object.entries(code.components)) {
|
|
1287
|
+
const value = resolve(handle, `code.components.${key}`);
|
|
1288
|
+
if (value !== void 0) components[key] = value;
|
|
1289
|
+
}
|
|
1290
|
+
if (Object.keys(components).length > 0) set("components", components);
|
|
1291
|
+
}
|
|
1292
|
+
if (code.routes) {
|
|
1293
|
+
const routes = [];
|
|
1294
|
+
for (const route of code.routes) {
|
|
1295
|
+
const component = resolve(
|
|
1296
|
+
route.component,
|
|
1297
|
+
`code.routes[${route.id}].component`
|
|
1298
|
+
);
|
|
1299
|
+
if (component === void 0) continue;
|
|
1300
|
+
routes.push(
|
|
1301
|
+
compact({
|
|
1302
|
+
id: route.id,
|
|
1303
|
+
path: route.path,
|
|
1304
|
+
kind: route.kind,
|
|
1305
|
+
chrome: route.chrome,
|
|
1306
|
+
component
|
|
1307
|
+
})
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
if (routes.length > 0) set("routes", routes);
|
|
1311
|
+
}
|
|
1312
|
+
if (code.searchRenderSuggestion) {
|
|
1313
|
+
const renderSuggestion = resolve(
|
|
1314
|
+
code.searchRenderSuggestion,
|
|
1315
|
+
"code.searchRenderSuggestion"
|
|
1316
|
+
);
|
|
1317
|
+
if (renderSuggestion !== void 0) {
|
|
1318
|
+
set("search", { ...doc.presentation?.search ?? {}, renderSuggestion });
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
if (code.footerCredit) {
|
|
1322
|
+
const credit = resolve(code.footerCredit, "code.footerCredit");
|
|
1323
|
+
if (credit !== void 0) {
|
|
1324
|
+
set("footer", { ...doc.presentation?.footer ?? {}, credit });
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
if (config.footer === void 0 && doc.presentation?.footer !== void 0) {
|
|
1329
|
+
set("footer", doc.presentation.footer);
|
|
1330
|
+
}
|
|
1331
|
+
return config;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
export { STOREFRONT_COMPONENT_KEYS, STOREFRONT_PAGE_KINDS, STORE_BACKEND_PROVIDERS, STORE_COMMERCE_MODES, STORE_DOCUMENT_KIND, STORE_PAYMENT_METHOD_KINDS, STORE_PAYMENT_MODES, STORE_ROUTE_CHROMES, STORE_ROUTE_KINDS, STORE_SECTIONS, STORE_SECTION_NAMES, exportStore, hydrateStoreDocument, isJsonValue, isRecord, sectionToJsonSchema, slugifyStoreName, validateStore };
|
|
1335
|
+
//# sourceMappingURL=chunk-GI5BTJK6.js.map
|
|
1336
|
+
//# sourceMappingURL=chunk-GI5BTJK6.js.map
|