@asteby/storefront-react 0.5.0 → 0.6.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/README.md +27 -0
- package/dist/index.cjs +167 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -1
- package/dist/index.d.ts +104 -1
- package/dist/index.js +163 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,6 +149,33 @@ defaults, coerces numeric strings, drops unknown props and skips a section
|
|
|
149
149
|
missing a required prop. `registry.toJSONSchema()` / `sectionJsonSchema(def)`
|
|
150
150
|
emit the JSON Schema the OPS editor reads (`section.schema.json`).
|
|
151
151
|
|
|
152
|
+
### Sincronizar tipos de sección con OPS
|
|
153
|
+
|
|
154
|
+
Convierte los `defineSection` al formato del addon `storefront_content` y los
|
|
155
|
+
publica sin escribir conversores a mano:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { createStorefrontClient } from '@asteby/storefront'
|
|
159
|
+
import { syncSectionTypes } from '@asteby/storefront-react'
|
|
160
|
+
|
|
161
|
+
const sf = createStorefrontClient({ baseUrl: process.env.OPS_GW!, token: process.env.OPS_SK! }) // sk_ con content:write
|
|
162
|
+
const { created, updated, warnings } = await syncSectionTypes(sf, registry)
|
|
163
|
+
// o sin cliente: syncSectionTypes({ baseUrl, token, fetch? }, registry)
|
|
164
|
+
// o solo exportar: registry.toOpsSectionTypes() -> { types, warnings }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Conversión: `items`→`fields` (recursivo), `url`/`color`→`text` (la URL viaja como
|
|
168
|
+
texto; `link` de OPS es `{label,url}`), `products`→`product_list`. `max` fija el
|
|
169
|
+
tope de texto en OPS. `defineSection` acepta `category` y `maxPerPage`.
|
|
170
|
+
|
|
171
|
+
Los tipos integrados de OPS (`hero, product_grid, collection_list, rich_text,
|
|
172
|
+
faq, image_with_text`) se excluyen (`excludeBuiltin`, por omisión `true`) y
|
|
173
|
+
generan `warnings`; pasa `builtinProps: { hero: ['title', ...] }` para avisar
|
|
174
|
+
solo de las props locales que no existen en OPS. Antes de llamar a OPS se valida
|
|
175
|
+
localmente (lista sin `fields`, `select` sin `options`, kind desconocido, clave no
|
|
176
|
+
snake_case, etiqueta vacía): lanza `SectionTypesValidationError` con `issues[]`
|
|
177
|
+
(`{path,message}`); también disponible como `validateOpsSectionTypes(types)`.
|
|
178
|
+
|
|
152
179
|
### Theme
|
|
153
180
|
|
|
154
181
|
`themeToCss(theme, { selector?, naming?, fontFallback? })` turns
|
package/dist/index.cjs
CHANGED
|
@@ -1010,6 +1010,156 @@ function JsonLd({ data, nonce }) {
|
|
|
1010
1010
|
if (!data || Array.isArray(data) && data.length === 0) return null;
|
|
1011
1011
|
return /* @__PURE__ */ jsxRuntime.jsx("script", { type: "application/ld+json", nonce, dangerouslySetInnerHTML: { __html: serializeJsonLd(data) } });
|
|
1012
1012
|
}
|
|
1013
|
+
var OPS_BUILTIN_SECTION_TYPES = ["hero", "product_grid", "collection_list", "rich_text", "faq", "image_with_text"];
|
|
1014
|
+
var OPS_PROP_KINDS = ["text", "textarea", "richtext", "image", "link", "number", "select", "boolean", "collection", "collection_list", "product", "product_list", "list"];
|
|
1015
|
+
var SectionTypesValidationError = class extends Error {
|
|
1016
|
+
issues;
|
|
1017
|
+
constructor(issues) {
|
|
1018
|
+
super(`Invalid section types for OPS:
|
|
1019
|
+
${issues.map((i) => ` - ${i.path}: ${i.message}`).join("\n")}`);
|
|
1020
|
+
this.name = "SectionTypesValidationError";
|
|
1021
|
+
this.issues = issues;
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
var SNAKE = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;
|
|
1025
|
+
var KIND_MAP = {
|
|
1026
|
+
text: "text",
|
|
1027
|
+
textarea: "textarea",
|
|
1028
|
+
richtext: "richtext",
|
|
1029
|
+
url: "text",
|
|
1030
|
+
color: "text",
|
|
1031
|
+
image: "image",
|
|
1032
|
+
number: "number",
|
|
1033
|
+
boolean: "boolean",
|
|
1034
|
+
select: "select",
|
|
1035
|
+
collection: "collection",
|
|
1036
|
+
product: "product",
|
|
1037
|
+
products: "product_list",
|
|
1038
|
+
list: "list"
|
|
1039
|
+
};
|
|
1040
|
+
var LIST_KINDS = /* @__PURE__ */ new Set(["list"]);
|
|
1041
|
+
function convertProps(schema, path, issues) {
|
|
1042
|
+
const out = [];
|
|
1043
|
+
for (const [key, spec] of Object.entries(schema ?? {})) {
|
|
1044
|
+
const p = `${path}.${key}`;
|
|
1045
|
+
const kind = KIND_MAP[spec.kind];
|
|
1046
|
+
if (!kind) {
|
|
1047
|
+
issues.push({ path: `${p}.kind`, message: `unknown kind "${String(spec.kind)}"` });
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
const prop = { key, kind, label: spec.label };
|
|
1051
|
+
if (spec.required) prop.required = true;
|
|
1052
|
+
if ("default" in spec && spec.default !== void 0) prop.default = spec.default;
|
|
1053
|
+
if (spec.options) prop.options = spec.options.map((o) => ({ value: o.value, label: o.label }));
|
|
1054
|
+
if (spec.min !== void 0) prop.min = spec.min;
|
|
1055
|
+
if (spec.max !== void 0) prop.max = spec.max;
|
|
1056
|
+
if (LIST_KINDS.has(kind)) prop.fields = convertProps(spec.items, `${p}.fields`, issues);
|
|
1057
|
+
out.push(prop);
|
|
1058
|
+
}
|
|
1059
|
+
return out;
|
|
1060
|
+
}
|
|
1061
|
+
function validateOpsSectionTypes(types) {
|
|
1062
|
+
const issues = [];
|
|
1063
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1064
|
+
const walk = (props, path) => {
|
|
1065
|
+
if (!Array.isArray(props)) {
|
|
1066
|
+
issues.push({ path, message: "must be a list" });
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1070
|
+
props.forEach((prop, i) => {
|
|
1071
|
+
const p = `${path}[${i}]`;
|
|
1072
|
+
if (typeof prop.key !== "string" || !SNAKE.test(prop.key)) issues.push({ path: `${p}.key`, message: `"${String(prop.key)}" must be snake_case (a-z, 0-9, _)` });
|
|
1073
|
+
else if (keys.has(prop.key)) issues.push({ path: `${p}.key`, message: `duplicate key "${prop.key}"` });
|
|
1074
|
+
else keys.add(prop.key);
|
|
1075
|
+
if (typeof prop.label !== "string" || !prop.label.trim()) issues.push({ path: `${p}.label`, message: "label is required" });
|
|
1076
|
+
if (!OPS_PROP_KINDS.includes(prop.kind)) {
|
|
1077
|
+
issues.push({ path: `${p}.kind`, message: `unknown kind "${String(prop.kind)}"` });
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (prop.kind === "list") {
|
|
1081
|
+
if (!prop.fields || prop.fields.length === 0) issues.push({ path: `${p}.fields`, message: 'list requires a non-empty "fields"' });
|
|
1082
|
+
else walk(prop.fields, `${p}.fields`);
|
|
1083
|
+
}
|
|
1084
|
+
if (prop.kind === "select" && (!prop.options || prop.options.length === 0)) issues.push({ path: `${p}.options`, message: 'select requires non-empty "options"' });
|
|
1085
|
+
if (prop.min !== void 0 && prop.max !== void 0 && prop.min > prop.max) issues.push({ path: `${p}.max`, message: "max must be >= min" });
|
|
1086
|
+
});
|
|
1087
|
+
};
|
|
1088
|
+
types.forEach((t, i) => {
|
|
1089
|
+
const p = t.type ? t.type : `[${i}]`;
|
|
1090
|
+
if (!t.type || !t.type.trim()) issues.push({ path: `${p}.type`, message: "type is required" });
|
|
1091
|
+
else if (seen.has(t.type)) issues.push({ path: `${p}.type`, message: `duplicate type "${t.type}"` });
|
|
1092
|
+
else seen.add(t.type);
|
|
1093
|
+
if (!t.label || !t.label.trim()) issues.push({ path: `${p}.label`, message: "label is required" });
|
|
1094
|
+
if (t.max_per_page !== void 0 && (!Number.isInteger(t.max_per_page) || t.max_per_page < 1)) issues.push({ path: `${p}.max_per_page`, message: "must be a positive integer" });
|
|
1095
|
+
walk(t.schema?.props, `${p}.schema.props`);
|
|
1096
|
+
});
|
|
1097
|
+
return issues;
|
|
1098
|
+
}
|
|
1099
|
+
function sectionTypesToOps(source, options = {}) {
|
|
1100
|
+
const excludeBuiltin = options.excludeBuiltin ?? true;
|
|
1101
|
+
const defs = Array.isArray(source) ? source : source.definitions();
|
|
1102
|
+
const builtin = new Set(OPS_BUILTIN_SECTION_TYPES);
|
|
1103
|
+
const issues = [];
|
|
1104
|
+
const warnings = [];
|
|
1105
|
+
const types = [];
|
|
1106
|
+
for (const def of defs) {
|
|
1107
|
+
if (excludeBuiltin && builtin.has(def.type)) {
|
|
1108
|
+
const known = options.builtinProps?.[def.type];
|
|
1109
|
+
if (known) {
|
|
1110
|
+
for (const name of Object.keys(def.schema ?? {})) {
|
|
1111
|
+
if (!known.includes(name)) warnings.push({ type: def.type, prop: name, message: `prop "${name}" of built-in type "${def.type}" does not exist in the OPS schema; built-in types cannot be redefined, so it will not be editable in OPS` });
|
|
1112
|
+
}
|
|
1113
|
+
} else {
|
|
1114
|
+
warnings.push({ type: def.type, message: `"${def.type}" is a built-in OPS type: not synced, and its local props cannot be verified against the OPS schema` });
|
|
1115
|
+
}
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
const entryIssues = [];
|
|
1119
|
+
const props = convertProps(def.schema, `${def.type}.schema.props`, entryIssues);
|
|
1120
|
+
issues.push(...entryIssues);
|
|
1121
|
+
const out = { type: def.type, label: def.label ?? def.type, schema: { props } };
|
|
1122
|
+
if (def.description) out.description = def.description;
|
|
1123
|
+
if (def.category) out.category = def.category;
|
|
1124
|
+
if (def.maxPerPage !== void 0) out.max_per_page = def.maxPerPage;
|
|
1125
|
+
types.push(out);
|
|
1126
|
+
}
|
|
1127
|
+
issues.push(...validateOpsSectionTypes(types));
|
|
1128
|
+
if (issues.length) throw new SectionTypesValidationError(issues);
|
|
1129
|
+
return { types, warnings };
|
|
1130
|
+
}
|
|
1131
|
+
var SYNC_PATH = "/storefront/content/section-types";
|
|
1132
|
+
var count = (v) => Array.isArray(v) ? v.length : typeof v === "number" ? v : 0;
|
|
1133
|
+
async function syncSectionTypes(target, source, options = {}) {
|
|
1134
|
+
const { types, warnings } = sectionTypesToOps(source, options);
|
|
1135
|
+
let raw;
|
|
1136
|
+
if ("request" in target && typeof target.request === "function") {
|
|
1137
|
+
raw = await target.request({ method: "PUT", path: SYNC_PATH, body: { section_types: types } });
|
|
1138
|
+
} else {
|
|
1139
|
+
const t = target;
|
|
1140
|
+
if (!t.token.startsWith("sk_")) throw new TypeError("syncSectionTypes: an sk_ token with content:write is required");
|
|
1141
|
+
const f = t.fetch ?? globalThis.fetch.bind(globalThis);
|
|
1142
|
+
const url = t.baseUrl.replace(/\/+$/, "") + SYNC_PATH;
|
|
1143
|
+
const res = await f(url, {
|
|
1144
|
+
method: "PUT",
|
|
1145
|
+
headers: { Authorization: `Bearer ${t.token}`, "Content-Type": "application/json", Accept: "application/json" },
|
|
1146
|
+
body: JSON.stringify({ section_types: types })
|
|
1147
|
+
});
|
|
1148
|
+
const text = await res.text();
|
|
1149
|
+
let body;
|
|
1150
|
+
try {
|
|
1151
|
+
body = text ? JSON.parse(text) : void 0;
|
|
1152
|
+
} catch {
|
|
1153
|
+
body = void 0;
|
|
1154
|
+
}
|
|
1155
|
+
if (!res.ok) {
|
|
1156
|
+
throw new storefront.StorefrontError({ status: res.status, code: body?.code ?? body?.error?.code ?? "unknown", message: body?.message ?? body?.error?.message ?? `Section types sync failed (${res.status})`, url });
|
|
1157
|
+
}
|
|
1158
|
+
raw = body;
|
|
1159
|
+
}
|
|
1160
|
+
const data = raw && typeof raw === "object" && "data" in raw ? raw.data : raw;
|
|
1161
|
+
return { created: count(data?.created), updated: count(data?.updated), warnings };
|
|
1162
|
+
}
|
|
1013
1163
|
function defineSection(definition) {
|
|
1014
1164
|
return definition;
|
|
1015
1165
|
}
|
|
@@ -1033,6 +1183,9 @@ function resolveSectionProps(schema, raw, path = "") {
|
|
|
1033
1183
|
else if (spec.required) errors.push({ prop: key, message: "is required" });
|
|
1034
1184
|
continue;
|
|
1035
1185
|
}
|
|
1186
|
+
if (spec.kind === "image" && typeof value === "object" && value !== null && typeof value.url === "string") {
|
|
1187
|
+
value = value.url;
|
|
1188
|
+
}
|
|
1036
1189
|
if (STRING_KINDS.has(spec.kind)) {
|
|
1037
1190
|
if (typeof value !== "string") {
|
|
1038
1191
|
fail2("must be a string");
|
|
@@ -1156,6 +1309,14 @@ var SectionRegistry = class {
|
|
|
1156
1309
|
toJSONSchema() {
|
|
1157
1310
|
return Object.fromEntries(this.definitions().map((d) => [d.type, sectionJsonSchema(d)]));
|
|
1158
1311
|
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Section types in the OPS `storefront_content` format, ready for
|
|
1314
|
+
* `PUT /storefront/content/section-types`. Throws `SectionTypesValidationError`
|
|
1315
|
+
* on anything OPS would reject; `warnings` flags built-in types.
|
|
1316
|
+
*/
|
|
1317
|
+
toOpsSectionTypes(options) {
|
|
1318
|
+
return sectionTypesToOps(this, options);
|
|
1319
|
+
}
|
|
1159
1320
|
};
|
|
1160
1321
|
function createSectionRegistry(definitions = []) {
|
|
1161
1322
|
return new SectionRegistry(definitions);
|
|
@@ -1596,6 +1757,8 @@ exports.LocationList = LocationList;
|
|
|
1596
1757
|
exports.LocationPicker = LocationPicker;
|
|
1597
1758
|
exports.Money = Money;
|
|
1598
1759
|
exports.NearestLocationForm = NearestLocationForm;
|
|
1760
|
+
exports.OPS_BUILTIN_SECTION_TYPES = OPS_BUILTIN_SECTION_TYPES;
|
|
1761
|
+
exports.OPS_PROP_KINDS = OPS_PROP_KINDS;
|
|
1599
1762
|
exports.OpenStatus = OpenStatus;
|
|
1600
1763
|
exports.PickupOption = PickupOption;
|
|
1601
1764
|
exports.ProductPrice = ProductPrice;
|
|
@@ -1603,6 +1766,7 @@ exports.RestockNotifyForm = RestockNotifyForm;
|
|
|
1603
1766
|
exports.SELECTED_LOCATION_PARAM = SELECTED_LOCATION_PARAM;
|
|
1604
1767
|
exports.SELECTED_LOCATION_STORAGE_KEY = SELECTED_LOCATION_STORAGE_KEY;
|
|
1605
1768
|
exports.SectionRegistry = SectionRegistry;
|
|
1769
|
+
exports.SectionTypesValidationError = SectionTypesValidationError;
|
|
1606
1770
|
exports.Sections = Sections;
|
|
1607
1771
|
exports.SrOnly = SrOnly;
|
|
1608
1772
|
exports.StockConflictAlert = StockConflictAlert;
|
|
@@ -1629,9 +1793,11 @@ exports.readCartCookie = readCartCookie;
|
|
|
1629
1793
|
exports.readSelectedLocationId = readSelectedLocationId;
|
|
1630
1794
|
exports.resolveSectionProps = resolveSectionProps;
|
|
1631
1795
|
exports.sectionJsonSchema = sectionJsonSchema;
|
|
1796
|
+
exports.sectionTypesToOps = sectionTypesToOps;
|
|
1632
1797
|
exports.seoMeta = seoMeta;
|
|
1633
1798
|
exports.serializeCartCookie = serializeCartCookie;
|
|
1634
1799
|
exports.serializeJsonLd = serializeJsonLd;
|
|
1800
|
+
exports.syncSectionTypes = syncSectionTypes;
|
|
1635
1801
|
exports.themeFontsHref = themeFontsHref;
|
|
1636
1802
|
exports.themeToCss = themeToCss;
|
|
1637
1803
|
exports.themeToCssVariables = themeToCssVariables;
|
|
@@ -1647,6 +1813,7 @@ exports.useProductOptions = useProductOptions;
|
|
|
1647
1813
|
exports.useSelectedLocation = useSelectedLocation;
|
|
1648
1814
|
exports.useStorefront = useStorefront;
|
|
1649
1815
|
exports.useStorefrontClient = useStorefrontClient;
|
|
1816
|
+
exports.validateOpsSectionTypes = validateOpsSectionTypes;
|
|
1650
1817
|
exports.writeCartCookie = writeCartCookie;
|
|
1651
1818
|
//# sourceMappingURL=index.cjs.map
|
|
1652
1819
|
//# sourceMappingURL=index.cjs.map
|