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.
package/dist/index.cjs CHANGED
@@ -662,6 +662,35 @@ async function readImageSize(file) {
662
662
  bitmap.close?.();
663
663
  }
664
664
  }
665
+ /** Draws `crop` of the bitmap at `width × height` and encodes it (one resample). */
666
+ async function drawCrop(bitmap, crop, width, height, type, quality, fn) {
667
+ const { sx, sy, sw, sh } = crop;
668
+ if (typeof OffscreenCanvas === "function") {
669
+ const canvas$1 = new OffscreenCanvas(width, height);
670
+ const ctx$1 = canvas$1.getContext("2d");
671
+ if (!ctx$1) throw new Error(`${fn}: no 2D canvas context`);
672
+ ctx$1.imageSmoothingQuality = "high";
673
+ ctx$1.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
674
+ return canvas$1.convertToBlob({
675
+ type,
676
+ quality
677
+ });
678
+ }
679
+ const canvas = document.createElement("canvas");
680
+ canvas.width = width;
681
+ canvas.height = height;
682
+ const ctx = canvas.getContext("2d");
683
+ if (!ctx) throw new Error(`${fn}: no 2D canvas context`);
684
+ ctx.imageSmoothingQuality = "high";
685
+ ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
686
+ return new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error(`${fn}: encoding failed`)), type, quality));
687
+ }
688
+ /** The input's name with the output type's extension. */
689
+ function outputName(file, type) {
690
+ const name = file.name || "image";
691
+ const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
692
+ return `${name.replace(/\.[^.]+$/, "")}.${ext}`;
693
+ }
665
694
  /**
666
695
  * Returns the image at exactly `width × height`: scaled and centre-cropped
667
696
  * (never stretched). Browser only. The result is a `File` named like the
@@ -670,39 +699,194 @@ async function readImageSize(file) {
670
699
  async function fitImageToSize(file, width, height, options = {}) {
671
700
  requireBrowserImages("fitImageToSize");
672
701
  const { type = "image/png", quality, keepIfExact = true } = options;
673
- const name = file.name || "image";
674
- const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
675
- const outName = `${name.replace(/\.[^.]+$/, "")}.${ext}`;
702
+ const outName = outputName(file, type);
676
703
  const bitmap = await createImageBitmap(file);
677
704
  try {
678
705
  if (keepIfExact && bitmap.width === width && bitmap.height === height && file.type === type) return file instanceof File ? file : new File([file], outName, { type });
679
- const { sx, sy, sw, sh } = coverCrop(bitmap.width, bitmap.height, width, height);
680
- let blob;
681
- if (typeof OffscreenCanvas === "function") {
682
- const canvas = new OffscreenCanvas(width, height);
683
- const ctx = canvas.getContext("2d");
684
- if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
685
- ctx.imageSmoothingQuality = "high";
686
- ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
687
- blob = await canvas.convertToBlob({
688
- type,
689
- quality
690
- });
691
- } else {
692
- const canvas = document.createElement("canvas");
693
- canvas.width = width;
694
- canvas.height = height;
695
- const ctx = canvas.getContext("2d");
696
- if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
697
- ctx.imageSmoothingQuality = "high";
698
- ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
699
- blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error("fitImageToSize: encoding failed")), type, quality));
700
- }
706
+ const blob = await drawCrop(bitmap, coverCrop(bitmap.width, bitmap.height, width, height), width, height, type, quality, "fitImageToSize");
701
707
  return new File([blob], outName, { type });
702
708
  } finally {
703
709
  bitmap.close?.();
704
710
  }
705
711
  }
712
+ /** A rule that can't be met by any image (a mistake in the rule, not the image). */
713
+ var ImageRuleError = class extends Error {
714
+ constructor(message) {
715
+ super(message);
716
+ this.name = "ImageRuleError";
717
+ }
718
+ };
719
+ /** The image is smaller than the rule's minimum (and `allowUpscale` is off). */
720
+ var ImageTooSmallError = class extends Error {
721
+ /** The smallest size the rule accepts. */
722
+ required;
723
+ /** The largest area of the right shape the image could give. */
724
+ actual;
725
+ constructor(required, actual) {
726
+ super(`The image is too small: it needs to be at least ${required.width} × ${required.height}, and it's ${actual.width} × ${actual.height}`);
727
+ this.name = "ImageTooSmallError";
728
+ this.required = required;
729
+ this.actual = actual;
730
+ }
731
+ };
732
+ const isPositive = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
733
+ /**
734
+ * An aspect ratio as width ÷ height: `"100:77"`, `"100/77"`, `[100, 77]` →
735
+ * 1.2987…; `1.3` → 1.3. Throws {@link ImageRuleError} for anything else.
736
+ */
737
+ function parseAspectRatio(value) {
738
+ let parts = null;
739
+ if (typeof value === "number") return isPositive(value) ? value : badRatio(value);
740
+ if (Array.isArray(value)) parts = [value[0], value[1]];
741
+ else if (typeof value === "string") {
742
+ const text$1 = value.trim();
743
+ const match = /^(\d+(?:\.\d+)?)\s*[:/x×]\s*(\d+(?:\.\d+)?)$/i.exec(text$1);
744
+ if (match) parts = [Number(match[1]), Number(match[2])];
745
+ else if (/^\d+(?:\.\d+)?$/.test(text$1)) return isPositive(Number(text$1)) ? Number(text$1) : badRatio(value);
746
+ }
747
+ if (parts && isPositive(parts[0]) && isPositive(parts[1])) return parts[0] / parts[1];
748
+ return badRatio(value);
749
+ }
750
+ function badRatio(value) {
751
+ throw new ImageRuleError(`Not an aspect ratio: ${JSON.stringify(value)} (use e.g. "100:77", [100, 77] or 1.3)`);
752
+ }
753
+ /** Throws {@link ImageRuleError} when the rule contradicts itself. */
754
+ function checkImageRule(rule) {
755
+ if (!rule || typeof rule !== "object") throw new ImageRuleError("An image rule must be an object");
756
+ for (const key of [
757
+ "width",
758
+ "height",
759
+ "minWidth",
760
+ "minHeight",
761
+ "maxWidth",
762
+ "maxHeight",
763
+ "maxLongSide"
764
+ ]) if (rule[key] != null && !isPositive(rule[key])) throw new ImageRuleError(`${key} must be a positive number`);
765
+ if (rule.width != null || rule.height != null) {
766
+ if (rule.width == null || rule.height == null) throw new ImageRuleError("An exact size needs both width and height");
767
+ if (rule.aspectRatio != null) throw new ImageRuleError("Give an exact size or an aspectRatio, not both (the size sets the ratio)");
768
+ const extra = [
769
+ "minWidth",
770
+ "minHeight",
771
+ "maxWidth",
772
+ "maxHeight",
773
+ "maxLongSide"
774
+ ].filter((key) => rule[key] != null);
775
+ if (extra.length > 0) throw new ImageRuleError(`An exact size is already the minimum and the maximum; remove ${extra.join(", ")}`);
776
+ return;
777
+ }
778
+ if (rule.aspectRatio != null) parseAspectRatio(rule.aspectRatio);
779
+ if (rule.minWidth != null && rule.maxWidth != null && rule.minWidth > rule.maxWidth) throw new ImageRuleError("minWidth is larger than maxWidth");
780
+ if (rule.minHeight != null && rule.maxHeight != null && rule.minHeight > rule.maxHeight) throw new ImageRuleError("minHeight is larger than maxHeight");
781
+ if (rule.maxLongSide != null && Math.max(rule.minWidth ?? 0, rule.minHeight ?? 0) > rule.maxLongSide) throw new ImageRuleError("The minimum size is larger than maxLongSide");
782
+ }
783
+ /**
784
+ * Works out what to do to a `source`-sized image to meet `rule`: the centred
785
+ * crop of the right shape (the largest one), then the output size (scaled
786
+ * down to the maximum, never below the minimum). Pure: no browser needed.
787
+ *
788
+ * Throws {@link ImageTooSmallError} when the crop is below the minimum (or
789
+ * the exact size) and `allowUpscale` is off, and {@link ImageRuleError} for a
790
+ * contradictory rule.
791
+ *
792
+ * @example
793
+ * planImageFit({ width: 6000, height: 4000 }, { aspectRatio: "100:77", minWidth: 600, minHeight: 462, maxLongSide: 2000 })
794
+ * // → crop { sx: 403, sy: 0, sw: 5194, sh: 4000 }, 2000 × 1540
795
+ */
796
+ function planImageFit(source, rule) {
797
+ checkImageRule(rule);
798
+ const { width: W, height: H } = source ?? {};
799
+ if (!isPositive(W) || !isPositive(H)) throw new Error("planImageFit: the image has no size");
800
+ const exact = rule.width != null && rule.height != null;
801
+ const ratio = exact ? rule.width / rule.height : rule.aspectRatio != null ? parseAspectRatio(rule.aspectRatio) : null;
802
+ const crop = ratio == null ? {
803
+ sx: 0,
804
+ sy: 0,
805
+ sw: W,
806
+ sh: H
807
+ } : coverCrop(W, H, ratio, 1);
808
+ const { sw, sh } = crop;
809
+ const kept = {
810
+ width: sw,
811
+ height: sh
812
+ };
813
+ let width;
814
+ let height;
815
+ if (exact) {
816
+ width = rule.width;
817
+ height = rule.height;
818
+ if ((sw < width || sh < height) && !rule.allowUpscale) throw new ImageTooSmallError({
819
+ width,
820
+ height
821
+ }, kept);
822
+ } else {
823
+ const minW = rule.minWidth ?? 0;
824
+ const minH = rule.minHeight ?? 0;
825
+ if ((sw < minW || sh < minH) && !rule.allowUpscale) throw new ImageTooSmallError(requiredSize(minW, minH, ratio), kept);
826
+ let scale = 1;
827
+ if (rule.maxWidth != null) scale = Math.min(scale, rule.maxWidth / sw);
828
+ if (rule.maxHeight != null) scale = Math.min(scale, rule.maxHeight / sh);
829
+ if (rule.maxLongSide != null) scale = Math.min(scale, rule.maxLongSide / Math.max(sw, sh));
830
+ scale = Math.max(scale, minW / sw, minH / sh);
831
+ width = Math.max(1, Math.round(sw * scale));
832
+ height = Math.max(1, Math.round(sh * scale));
833
+ }
834
+ const unchanged = crop.sx === 0 && crop.sy === 0 && sw === W && sh === H && width === W && height === H;
835
+ return {
836
+ crop,
837
+ width,
838
+ height,
839
+ unchanged
840
+ };
841
+ }
842
+ /** The minimum as a full size: a ratio rule with only one minimum implies the other. */
843
+ function requiredSize(minW, minH, ratio) {
844
+ if (ratio == null) return {
845
+ width: minW,
846
+ height: minH
847
+ };
848
+ return {
849
+ width: Math.max(minW, Math.ceil(minH * ratio)),
850
+ height: Math.max(minH, Math.ceil(minW / ratio))
851
+ };
852
+ }
853
+ const OUTPUT_TYPES = [
854
+ "image/png",
855
+ "image/jpeg",
856
+ "image/webp"
857
+ ];
858
+ /**
859
+ * Returns the image meeting `rule` ({@link planImageFit}): centre-cropped to
860
+ * the right shape and scaled in a single draw. An image that already fits is
861
+ * returned untouched (same bytes). Browser only.
862
+ *
863
+ * Throws {@link ImageTooSmallError} (show `required` and `actual` to the
864
+ * person) or {@link ImageRuleError}; an unreadable file rejects with the
865
+ * browser's decoding error.
866
+ *
867
+ * @example
868
+ * const ready = await prepareImage(file, { aspectRatio: "100:77", minWidth: 600, minHeight: 462, maxLongSide: 2000 });
869
+ * await uploadImage(ready, { kind: "product", prefix: "product" });
870
+ */
871
+ async function prepareImage(file, rule, options = {}) {
872
+ checkImageRule(rule);
873
+ requireBrowserImages("prepareImage");
874
+ const sourceType = file.type && OUTPUT_TYPES.includes(file.type) ? file.type : "image/png";
875
+ const type = options.type ?? sourceType;
876
+ const quality = options.quality ?? (type === "image/png" ? void 0 : .9);
877
+ const bitmap = await createImageBitmap(file);
878
+ try {
879
+ const plan = planImageFit({
880
+ width: bitmap.width,
881
+ height: bitmap.height
882
+ }, rule);
883
+ if (plan.unchanged && type === file.type) return file instanceof File ? file : new File([file], outputName(file, type), { type });
884
+ const blob = await drawCrop(bitmap, plan.crop, plan.width, plan.height, type, quality, "prepareImage");
885
+ return new File([blob], outputName(file, type), { type });
886
+ } finally {
887
+ bitmap.close?.();
888
+ }
889
+ }
706
890
 
707
891
  //#endregion
708
892
  //#region src/core/upload.ts
@@ -716,7 +900,7 @@ const STATIC_HOST = /^https?:\/\/static\.merchantstack\.com\/images\//i;
716
900
  /**
717
901
  * Turns an image path from the API into a URL a page can load over HTTPS:
718
902
  * `http://static.merchantstack.com/images/x.png` → `<base>/x.png`, and a
719
- * relative path (`merchant/5644/logo/x.png`) → `<base>/merchant/5644/logo/x.png`.
903
+ * relative path (`merchant/123/logo/x.png`) → `<base>/merchant/123/logo/x.png`.
720
904
  * Other absolute URLs are returned unchanged; empty input gives `""`.
721
905
  */
722
906
  function getImageUrl(path, base = SHOPWAVE_IMAGE_BASE_URL) {
@@ -768,7 +952,7 @@ const MERCHANT_IMAGE_SLOT_KEYS = [
768
952
  "featured"
769
953
  ];
770
954
  /**
771
- * Where each slot lives, its upload kind and its exact size (from adminV1's
955
+ * Where each slot lives, its upload kind and its exact size (from the existing admin's
772
956
  * merchant settings page).
773
957
  */
774
958
  const MERCHANT_IMAGE_SLOTS = Object.freeze({
@@ -859,7 +1043,7 @@ function toStored(value) {
859
1043
  }
860
1044
  /**
861
1045
  * The `imageIds` to send on a save: every URL is turned back into its file
862
- * name (what adminV1 does with images you didn't change) and empty values
1046
+ * name (what the existing admin does with images you didn't change) and empty values
863
1047
  * are dropped. No images gives `{}`.
864
1048
  */
865
1049
  function toStoredImageIds(imageIds) {
@@ -880,7 +1064,7 @@ async function uploadMerchantImage(file, slot, options = {}) {
880
1064
  //#endregion
881
1065
  //#region src/core/productImages.ts
882
1066
  /**
883
- * Product images, as the live API stores them (merchant 5644, 4 Nov 2026):
1067
+ * Product images, as the live API stores them (4 Nov 2026):
884
1068
  * - A product's `images` is a list. **Reads return full URLs**
885
1069
  * (`http://static.merchantstack.com/images/merchant/<id>/product/<file>`);
886
1070
  * **writes take the uploader's file names** ({@link toStoredProductImages};
@@ -1282,9 +1466,9 @@ function trimmed(value) {
1282
1466
  /**
1283
1467
  * Reads `merchant.links` as flat fields (`{ website, twitter, facebook,
1284
1468
  * instagram }`), whatever shape it was saved in:
1285
- * - the nested shape adminV1 saves (`{ website: { home }, social: { … } }`);
1469
+ * - the nested shape the existing admin saves (`{ website: { home }, social: { … } }`);
1286
1470
  * - flat keys (`{ website: "…" }`, what SDK 0.4.0 pre-releases sent);
1287
- * - an array in {@link MERCHANT_LINK_KEYS} order (older AdminUI versions).
1471
+ * - an array in {@link MERCHANT_LINK_KEYS} order (older app versions).
1288
1472
  * Empty values are dropped.
1289
1473
  */
1290
1474
  function merchantLinksToForm(links) {
@@ -1321,7 +1505,7 @@ function merchantLinksFromForm(form) {
1321
1505
  }
1322
1506
  return out;
1323
1507
  }
1324
- /** Reads any stored or older `links` value into the nested shape adminV1 saves. */
1508
+ /** Reads any stored or older `links` value into the nested shape the existing admin saves. */
1325
1509
  function normalizeMerchantLinks(links) {
1326
1510
  return merchantLinksFromForm(merchantLinksToForm(links));
1327
1511
  }
@@ -1997,6 +2181,8 @@ function useBasketReport(options) {
1997
2181
  //#endregion
1998
2182
  exports.DEFAULT_BASKET_FILTERS = DEFAULT_BASKET_FILTERS;
1999
2183
  exports.DEFAULT_PAGINATION = DEFAULT_PAGINATION;
2184
+ exports.ImageRuleError = ImageRuleError;
2185
+ exports.ImageTooSmallError = ImageTooSmallError;
2000
2186
  exports.MERCHANT_COLOUR_KEYS = MERCHANT_COLOUR_KEYS;
2001
2187
  exports.MERCHANT_FIELD_MAX_LENGTH = MERCHANT_FIELD_MAX_LENGTH;
2002
2188
  exports.MERCHANT_IMAGE_SLOTS = MERCHANT_IMAGE_SLOTS;
@@ -2022,6 +2208,7 @@ exports.assertNoApiErrors = assertNoApiErrors;
2022
2208
  exports.authorizationFor = authorizationFor;
2023
2209
  exports.buildBasketReportQuery = buildBasketReportQuery;
2024
2210
  exports.buildConsumerMap = buildConsumerMap;
2211
+ exports.checkImageRule = checkImageRule;
2025
2212
  exports.collectConsumerIds = collectConsumerIds;
2026
2213
  exports.collectProductIds = collectProductIds;
2027
2214
  exports.combineBasketRows = combineBasketRows;
@@ -2073,7 +2260,10 @@ exports.merchantLinksFromForm = merchantLinksFromForm;
2073
2260
  exports.merchantLinksToForm = merchantLinksToForm;
2074
2261
  exports.normalizeMerchantLinks = normalizeMerchantLinks;
2075
2262
  exports.normalizeTagList = normalizeTagList;
2263
+ exports.parseAspectRatio = parseAspectRatio;
2076
2264
  exports.parseBasketReportData = parseBasketReportData;
2265
+ exports.planImageFit = planImageFit;
2266
+ exports.prepareImage = prepareImage;
2077
2267
  exports.productImageRole = productImageRole;
2078
2268
  exports.readImageSize = readImageSize;
2079
2269
  exports.rolesById = rolesById;