react-shopwave-connect 0.4.4 → 0.4.6

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.
@@ -636,6 +636,35 @@ async function readImageSize(file) {
636
636
  bitmap.close?.();
637
637
  }
638
638
  }
639
+ /** Draws `crop` of the bitmap at `width × height` and encodes it (one resample). */
640
+ async function drawCrop(bitmap, crop, width, height, type, quality, fn) {
641
+ const { sx, sy, sw, sh } = crop;
642
+ if (typeof OffscreenCanvas === "function") {
643
+ const canvas$1 = new OffscreenCanvas(width, height);
644
+ const ctx$1 = canvas$1.getContext("2d");
645
+ if (!ctx$1) throw new Error(`${fn}: no 2D canvas context`);
646
+ ctx$1.imageSmoothingQuality = "high";
647
+ ctx$1.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
648
+ return canvas$1.convertToBlob({
649
+ type,
650
+ quality
651
+ });
652
+ }
653
+ const canvas = document.createElement("canvas");
654
+ canvas.width = width;
655
+ canvas.height = height;
656
+ const ctx = canvas.getContext("2d");
657
+ if (!ctx) throw new Error(`${fn}: no 2D canvas context`);
658
+ ctx.imageSmoothingQuality = "high";
659
+ ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
660
+ return new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error(`${fn}: encoding failed`)), type, quality));
661
+ }
662
+ /** The input's name with the output type's extension. */
663
+ function outputName(file, type) {
664
+ const name = file.name || "image";
665
+ const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
666
+ return `${name.replace(/\.[^.]+$/, "")}.${ext}`;
667
+ }
639
668
  /**
640
669
  * Returns the image at exactly `width × height`: scaled and centre-cropped
641
670
  * (never stretched). Browser only. The result is a `File` named like the
@@ -644,39 +673,194 @@ async function readImageSize(file) {
644
673
  async function fitImageToSize(file, width, height, options = {}) {
645
674
  requireBrowserImages("fitImageToSize");
646
675
  const { type = "image/png", quality, keepIfExact = true } = options;
647
- const name = file.name || "image";
648
- const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
649
- const outName = `${name.replace(/\.[^.]+$/, "")}.${ext}`;
676
+ const outName = outputName(file, type);
650
677
  const bitmap = await createImageBitmap(file);
651
678
  try {
652
679
  if (keepIfExact && bitmap.width === width && bitmap.height === height && file.type === type) return file instanceof File ? file : new File([file], outName, { type });
653
- const { sx, sy, sw, sh } = coverCrop(bitmap.width, bitmap.height, width, height);
654
- let blob;
655
- if (typeof OffscreenCanvas === "function") {
656
- const canvas = new OffscreenCanvas(width, height);
657
- const ctx = canvas.getContext("2d");
658
- if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
659
- ctx.imageSmoothingQuality = "high";
660
- ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
661
- blob = await canvas.convertToBlob({
662
- type,
663
- quality
664
- });
665
- } else {
666
- const canvas = document.createElement("canvas");
667
- canvas.width = width;
668
- canvas.height = height;
669
- const ctx = canvas.getContext("2d");
670
- if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
671
- ctx.imageSmoothingQuality = "high";
672
- ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
673
- blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error("fitImageToSize: encoding failed")), type, quality));
674
- }
680
+ const blob = await drawCrop(bitmap, coverCrop(bitmap.width, bitmap.height, width, height), width, height, type, quality, "fitImageToSize");
675
681
  return new File([blob], outName, { type });
676
682
  } finally {
677
683
  bitmap.close?.();
678
684
  }
679
685
  }
686
+ /** A rule that can't be met by any image (a mistake in the rule, not the image). */
687
+ var ImageRuleError = class extends Error {
688
+ constructor(message) {
689
+ super(message);
690
+ this.name = "ImageRuleError";
691
+ }
692
+ };
693
+ /** The image is smaller than the rule's minimum (and `allowUpscale` is off). */
694
+ var ImageTooSmallError = class extends Error {
695
+ /** The smallest size the rule accepts. */
696
+ required;
697
+ /** The largest area of the right shape the image could give. */
698
+ actual;
699
+ constructor(required, actual) {
700
+ super(`The image is too small: it needs to be at least ${required.width} × ${required.height}, and it's ${actual.width} × ${actual.height}`);
701
+ this.name = "ImageTooSmallError";
702
+ this.required = required;
703
+ this.actual = actual;
704
+ }
705
+ };
706
+ const isPositive = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
707
+ /**
708
+ * An aspect ratio as width ÷ height: `"100:77"`, `"100/77"`, `[100, 77]` →
709
+ * 1.2987…; `1.3` → 1.3. Throws {@link ImageRuleError} for anything else.
710
+ */
711
+ function parseAspectRatio(value) {
712
+ let parts = null;
713
+ if (typeof value === "number") return isPositive(value) ? value : badRatio(value);
714
+ if (Array.isArray(value)) parts = [value[0], value[1]];
715
+ else if (typeof value === "string") {
716
+ const text$1 = value.trim();
717
+ const match = /^(\d+(?:\.\d+)?)\s*[:/x×]\s*(\d+(?:\.\d+)?)$/i.exec(text$1);
718
+ if (match) parts = [Number(match[1]), Number(match[2])];
719
+ else if (/^\d+(?:\.\d+)?$/.test(text$1)) return isPositive(Number(text$1)) ? Number(text$1) : badRatio(value);
720
+ }
721
+ if (parts && isPositive(parts[0]) && isPositive(parts[1])) return parts[0] / parts[1];
722
+ return badRatio(value);
723
+ }
724
+ function badRatio(value) {
725
+ throw new ImageRuleError(`Not an aspect ratio: ${JSON.stringify(value)} (use e.g. "100:77", [100, 77] or 1.3)`);
726
+ }
727
+ /** Throws {@link ImageRuleError} when the rule contradicts itself. */
728
+ function checkImageRule(rule) {
729
+ if (!rule || typeof rule !== "object") throw new ImageRuleError("An image rule must be an object");
730
+ for (const key of [
731
+ "width",
732
+ "height",
733
+ "minWidth",
734
+ "minHeight",
735
+ "maxWidth",
736
+ "maxHeight",
737
+ "maxLongSide"
738
+ ]) if (rule[key] != null && !isPositive(rule[key])) throw new ImageRuleError(`${key} must be a positive number`);
739
+ if (rule.width != null || rule.height != null) {
740
+ if (rule.width == null || rule.height == null) throw new ImageRuleError("An exact size needs both width and height");
741
+ if (rule.aspectRatio != null) throw new ImageRuleError("Give an exact size or an aspectRatio, not both (the size sets the ratio)");
742
+ const extra = [
743
+ "minWidth",
744
+ "minHeight",
745
+ "maxWidth",
746
+ "maxHeight",
747
+ "maxLongSide"
748
+ ].filter((key) => rule[key] != null);
749
+ if (extra.length > 0) throw new ImageRuleError(`An exact size is already the minimum and the maximum; remove ${extra.join(", ")}`);
750
+ return;
751
+ }
752
+ if (rule.aspectRatio != null) parseAspectRatio(rule.aspectRatio);
753
+ if (rule.minWidth != null && rule.maxWidth != null && rule.minWidth > rule.maxWidth) throw new ImageRuleError("minWidth is larger than maxWidth");
754
+ if (rule.minHeight != null && rule.maxHeight != null && rule.minHeight > rule.maxHeight) throw new ImageRuleError("minHeight is larger than maxHeight");
755
+ if (rule.maxLongSide != null && Math.max(rule.minWidth ?? 0, rule.minHeight ?? 0) > rule.maxLongSide) throw new ImageRuleError("The minimum size is larger than maxLongSide");
756
+ }
757
+ /**
758
+ * Works out what to do to a `source`-sized image to meet `rule`: the centred
759
+ * crop of the right shape (the largest one), then the output size (scaled
760
+ * down to the maximum, never below the minimum). Pure: no browser needed.
761
+ *
762
+ * Throws {@link ImageTooSmallError} when the crop is below the minimum (or
763
+ * the exact size) and `allowUpscale` is off, and {@link ImageRuleError} for a
764
+ * contradictory rule.
765
+ *
766
+ * @example
767
+ * planImageFit({ width: 6000, height: 4000 }, { aspectRatio: "100:77", minWidth: 600, minHeight: 462, maxLongSide: 2000 })
768
+ * // → crop { sx: 403, sy: 0, sw: 5194, sh: 4000 }, 2000 × 1540
769
+ */
770
+ function planImageFit(source, rule) {
771
+ checkImageRule(rule);
772
+ const { width: W, height: H } = source ?? {};
773
+ if (!isPositive(W) || !isPositive(H)) throw new Error("planImageFit: the image has no size");
774
+ const exact = rule.width != null && rule.height != null;
775
+ const ratio = exact ? rule.width / rule.height : rule.aspectRatio != null ? parseAspectRatio(rule.aspectRatio) : null;
776
+ const crop = ratio == null ? {
777
+ sx: 0,
778
+ sy: 0,
779
+ sw: W,
780
+ sh: H
781
+ } : coverCrop(W, H, ratio, 1);
782
+ const { sw, sh } = crop;
783
+ const kept = {
784
+ width: sw,
785
+ height: sh
786
+ };
787
+ let width;
788
+ let height;
789
+ if (exact) {
790
+ width = rule.width;
791
+ height = rule.height;
792
+ if ((sw < width || sh < height) && !rule.allowUpscale) throw new ImageTooSmallError({
793
+ width,
794
+ height
795
+ }, kept);
796
+ } else {
797
+ const minW = rule.minWidth ?? 0;
798
+ const minH = rule.minHeight ?? 0;
799
+ if ((sw < minW || sh < minH) && !rule.allowUpscale) throw new ImageTooSmallError(requiredSize(minW, minH, ratio), kept);
800
+ let scale = 1;
801
+ if (rule.maxWidth != null) scale = Math.min(scale, rule.maxWidth / sw);
802
+ if (rule.maxHeight != null) scale = Math.min(scale, rule.maxHeight / sh);
803
+ if (rule.maxLongSide != null) scale = Math.min(scale, rule.maxLongSide / Math.max(sw, sh));
804
+ scale = Math.max(scale, minW / sw, minH / sh);
805
+ width = Math.max(1, Math.round(sw * scale));
806
+ height = Math.max(1, Math.round(sh * scale));
807
+ }
808
+ const unchanged = crop.sx === 0 && crop.sy === 0 && sw === W && sh === H && width === W && height === H;
809
+ return {
810
+ crop,
811
+ width,
812
+ height,
813
+ unchanged
814
+ };
815
+ }
816
+ /** The minimum as a full size: a ratio rule with only one minimum implies the other. */
817
+ function requiredSize(minW, minH, ratio) {
818
+ if (ratio == null) return {
819
+ width: minW,
820
+ height: minH
821
+ };
822
+ return {
823
+ width: Math.max(minW, Math.ceil(minH * ratio)),
824
+ height: Math.max(minH, Math.ceil(minW / ratio))
825
+ };
826
+ }
827
+ const OUTPUT_TYPES = [
828
+ "image/png",
829
+ "image/jpeg",
830
+ "image/webp"
831
+ ];
832
+ /**
833
+ * Returns the image meeting `rule` ({@link planImageFit}): centre-cropped to
834
+ * the right shape and scaled in a single draw. An image that already fits is
835
+ * returned untouched (same bytes). Browser only.
836
+ *
837
+ * Throws {@link ImageTooSmallError} (show `required` and `actual` to the
838
+ * person) or {@link ImageRuleError}; an unreadable file rejects with the
839
+ * browser's decoding error.
840
+ *
841
+ * @example
842
+ * const ready = await prepareImage(file, { aspectRatio: "100:77", minWidth: 600, minHeight: 462, maxLongSide: 2000 });
843
+ * await uploadImage(ready, { kind: "product", prefix: "product" });
844
+ */
845
+ async function prepareImage(file, rule, options = {}) {
846
+ checkImageRule(rule);
847
+ requireBrowserImages("prepareImage");
848
+ const sourceType = file.type && OUTPUT_TYPES.includes(file.type) ? file.type : "image/png";
849
+ const type = options.type ?? sourceType;
850
+ const quality = options.quality ?? (type === "image/png" ? void 0 : .9);
851
+ const bitmap = await createImageBitmap(file);
852
+ try {
853
+ const plan = planImageFit({
854
+ width: bitmap.width,
855
+ height: bitmap.height
856
+ }, rule);
857
+ if (plan.unchanged && type === file.type) return file instanceof File ? file : new File([file], outputName(file, type), { type });
858
+ const blob = await drawCrop(bitmap, plan.crop, plan.width, plan.height, type, quality, "prepareImage");
859
+ return new File([blob], outputName(file, type), { type });
860
+ } finally {
861
+ bitmap.close?.();
862
+ }
863
+ }
680
864
 
681
865
  //#endregion
682
866
  //#region src/core/upload.ts
@@ -690,7 +874,7 @@ const STATIC_HOST = /^https?:\/\/static\.merchantstack\.com\/images\//i;
690
874
  /**
691
875
  * Turns an image path from the API into a URL a page can load over HTTPS:
692
876
  * `http://static.merchantstack.com/images/x.png` → `<base>/x.png`, and a
693
- * relative path (`merchant/5644/logo/x.png`) → `<base>/merchant/5644/logo/x.png`.
877
+ * relative path (`merchant/123/logo/x.png`) → `<base>/merchant/123/logo/x.png`.
694
878
  * Other absolute URLs are returned unchanged; empty input gives `""`.
695
879
  */
696
880
  function getImageUrl(path, base = SHOPWAVE_IMAGE_BASE_URL) {
@@ -742,7 +926,7 @@ const MERCHANT_IMAGE_SLOT_KEYS = [
742
926
  "featured"
743
927
  ];
744
928
  /**
745
- * Where each slot lives, its upload kind and its exact size (from adminV1's
929
+ * Where each slot lives, its upload kind and its exact size (from the existing admin's
746
930
  * merchant settings page).
747
931
  */
748
932
  const MERCHANT_IMAGE_SLOTS = Object.freeze({
@@ -833,7 +1017,7 @@ function toStored(value) {
833
1017
  }
834
1018
  /**
835
1019
  * The `imageIds` to send on a save: every URL is turned back into its file
836
- * name (what adminV1 does with images you didn't change) and empty values
1020
+ * name (what the existing admin does with images you didn't change) and empty values
837
1021
  * are dropped. No images gives `{}`.
838
1022
  */
839
1023
  function toStoredImageIds(imageIds) {
@@ -854,7 +1038,7 @@ async function uploadMerchantImage(file, slot, options = {}) {
854
1038
  //#endregion
855
1039
  //#region src/core/productImages.ts
856
1040
  /**
857
- * Product images, as the live API stores them (merchant 5644, 4 Nov 2026):
1041
+ * Product images, as the live API stores them (4 Nov 2026):
858
1042
  * - A product's `images` is a list. **Reads return full URLs**
859
1043
  * (`http://static.merchantstack.com/images/merchant/<id>/product/<file>`);
860
1044
  * **writes take the uploader's file names** ({@link toStoredProductImages};
@@ -1256,9 +1440,9 @@ function trimmed(value) {
1256
1440
  /**
1257
1441
  * Reads `merchant.links` as flat fields (`{ website, twitter, facebook,
1258
1442
  * instagram }`), whatever shape it was saved in:
1259
- * - the nested shape adminV1 saves (`{ website: { home }, social: { … } }`);
1443
+ * - the nested shape the existing admin saves (`{ website: { home }, social: { … } }`);
1260
1444
  * - flat keys (`{ website: "…" }`, what SDK 0.4.0 pre-releases sent);
1261
- * - an array in {@link MERCHANT_LINK_KEYS} order (older AdminUI versions).
1445
+ * - an array in {@link MERCHANT_LINK_KEYS} order (older app versions).
1262
1446
  * Empty values are dropped.
1263
1447
  */
1264
1448
  function merchantLinksToForm(links) {
@@ -1295,7 +1479,7 @@ function merchantLinksFromForm(form) {
1295
1479
  }
1296
1480
  return out;
1297
1481
  }
1298
- /** Reads any stored or older `links` value into the nested shape adminV1 saves. */
1482
+ /** Reads any stored or older `links` value into the nested shape the existing admin saves. */
1299
1483
  function normalizeMerchantLinks(links) {
1300
1484
  return merchantLinksFromForm(merchantLinksToForm(links));
1301
1485
  }
@@ -1403,5 +1587,5 @@ async function fetchUser(options = {}) {
1403
1587
  }
1404
1588
 
1405
1589
  //#endregion
1406
- export { DEFAULT_BASKET_FILTERS, DEFAULT_PAGINATION, MERCHANT_COLOUR_KEYS, MERCHANT_FIELD_MAX_LENGTH, MERCHANT_IMAGE_SLOTS, MERCHANT_IMAGE_SLOT_KEYS, MERCHANT_LINK_KEYS, MERCHANT_LINK_PATHS, MERCHANT_OBJECT_FIELDS, MERCHANT_READ_ONLY_FIELDS, PromotionRuleSet, SHOPWAVE_ENTITIES, SHOPWAVE_IMAGE_BASE_URL, SHOPWAVE_LOOKUPS, SHOPWAVE_RESOURCES, SHOPWAVE_UPLOAD, ShopwaveApiError, UPLOAD_KINDS, UPLOAD_PREFIXES, apiDelete, apiGet, apiRequest, apiSend, assertNoApiErrors, authorizationFor, buildBasketReportQuery, buildConsumerMap, collectConsumerIds, collectProductIds, combineBasketRows, computeBasketSummary, coverCrop, deleteCategory, deleteEmployee, deleteEntity, deleteEntityById, deleteProduct, deletePromotion, deleteStore, fetchCategories, fetchCategory, fetchConsumer, fetchConsumers, fetchEmployee, fetchEmployees, fetchEntityById, fetchMerchant, fetchProduct, fetchProductInstanceTags, fetchProductTags, fetchProducts, fetchProductsMap, fetchPromotion, fetchPromotions, fetchReport, fetchSession, fetchStore, fetchStores, fetchUser, findRepricedInstances, fitImageToSize, formatDateForQuery, getApiErrorMap, getApiErrors, getImageUrl, getMerchantImage, getMerchantImageUrl, imageFileName, isSameInstancePrice, isUploadKind, isUploadPrefix, loginPath, logout, logoutPath, merchantLinksFromForm, merchantLinksToForm, normalizeMerchantLinks, normalizeTagList, parseBasketReportData, productImageRole, readImageSize, rolesById, saveCategory, saveEmployee, saveEntities, saveEntity, saveProduct, savePromotion, saveStore, setMerchantImage, submitEntity, toProductInstancesForSave, toShopwaveApiError, toStoredImageIds, toStoredProductImages, updateMerchant, uploadImage, uploadMerchantImage, validateMerchant, withToken };
1590
+ export { DEFAULT_BASKET_FILTERS, DEFAULT_PAGINATION, ImageRuleError, ImageTooSmallError, MERCHANT_COLOUR_KEYS, MERCHANT_FIELD_MAX_LENGTH, MERCHANT_IMAGE_SLOTS, MERCHANT_IMAGE_SLOT_KEYS, MERCHANT_LINK_KEYS, MERCHANT_LINK_PATHS, MERCHANT_OBJECT_FIELDS, MERCHANT_READ_ONLY_FIELDS, PromotionRuleSet, SHOPWAVE_ENTITIES, SHOPWAVE_IMAGE_BASE_URL, SHOPWAVE_LOOKUPS, SHOPWAVE_RESOURCES, SHOPWAVE_UPLOAD, ShopwaveApiError, UPLOAD_KINDS, UPLOAD_PREFIXES, apiDelete, apiGet, apiRequest, apiSend, assertNoApiErrors, authorizationFor, buildBasketReportQuery, buildConsumerMap, checkImageRule, collectConsumerIds, collectProductIds, combineBasketRows, computeBasketSummary, coverCrop, deleteCategory, deleteEmployee, deleteEntity, deleteEntityById, deleteProduct, deletePromotion, deleteStore, fetchCategories, fetchCategory, fetchConsumer, fetchConsumers, fetchEmployee, fetchEmployees, fetchEntityById, fetchMerchant, fetchProduct, fetchProductInstanceTags, fetchProductTags, fetchProducts, fetchProductsMap, fetchPromotion, fetchPromotions, fetchReport, fetchSession, fetchStore, fetchStores, fetchUser, findRepricedInstances, fitImageToSize, formatDateForQuery, getApiErrorMap, getApiErrors, getImageUrl, getMerchantImage, getMerchantImageUrl, imageFileName, isSameInstancePrice, isUploadKind, isUploadPrefix, loginPath, logout, logoutPath, merchantLinksFromForm, merchantLinksToForm, normalizeMerchantLinks, normalizeTagList, parseAspectRatio, parseBasketReportData, planImageFit, prepareImage, productImageRole, readImageSize, rolesById, saveCategory, saveEmployee, saveEntities, saveEntity, saveProduct, savePromotion, saveStore, setMerchantImage, submitEntity, toProductInstancesForSave, toShopwaveApiError, toStoredImageIds, toStoredProductImages, updateMerchant, uploadImage, uploadMerchantImage, validateMerchant, withToken };
1407
1591
  //# sourceMappingURL=index.js.map