shop-client 3.8.2 → 3.9.1

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.
Files changed (60) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +158 -1
  3. package/dist/ai/enrich.d.ts +93 -0
  4. package/dist/ai/enrich.js +25 -0
  5. package/dist/checkout.js +5 -114
  6. package/dist/{chunk-2KBOKOAD.mjs → chunk-2MF53V33.js} +32 -13
  7. package/dist/{chunk-BWKBRM2Z.mjs → chunk-CN7L3BHG.js} +12 -1
  8. package/dist/chunk-CXUCPK6X.js +460 -0
  9. package/dist/{chunk-QCTICSBE.mjs → chunk-MOBWPEY4.js} +29 -7
  10. package/dist/chunk-ROH545KI.js +274 -0
  11. package/dist/{chunk-QL5OUZGP.mjs → chunk-RR6YTQWP.js} +0 -1
  12. package/dist/{chunk-O4BPIIQ6.mjs → chunk-V52MFQZE.js} +11 -281
  13. package/dist/{chunk-WTK5HUFI.mjs → chunk-VPPCOJC3.js} +13 -435
  14. package/dist/collections.d.ts +2 -1
  15. package/dist/collections.js +7 -539
  16. package/dist/index.d.ts +28 -87
  17. package/dist/index.js +109 -2597
  18. package/dist/products.d.ts +2 -1
  19. package/dist/products.js +7 -1205
  20. package/dist/store.d.ts +53 -1
  21. package/dist/store.js +8 -697
  22. package/dist/{store-CJVUz2Yb.d.ts → types-luPg5O08.d.ts} +1 -208
  23. package/dist/utils/detect-country.d.ts +32 -0
  24. package/dist/utils/detect-country.js +6 -0
  25. package/dist/utils/func.d.ts +61 -0
  26. package/dist/utils/func.js +24 -0
  27. package/dist/utils/rate-limit.d.ts +5 -0
  28. package/dist/utils/rate-limit.js +7 -200
  29. package/package.json +21 -10
  30. package/dist/checkout.d.mts +0 -31
  31. package/dist/checkout.js.map +0 -1
  32. package/dist/checkout.mjs +0 -7
  33. package/dist/checkout.mjs.map +0 -1
  34. package/dist/chunk-2KBOKOAD.mjs.map +0 -1
  35. package/dist/chunk-BWKBRM2Z.mjs.map +0 -1
  36. package/dist/chunk-O4BPIIQ6.mjs.map +0 -1
  37. package/dist/chunk-QCTICSBE.mjs.map +0 -1
  38. package/dist/chunk-QL5OUZGP.mjs.map +0 -1
  39. package/dist/chunk-WTK5HUFI.mjs.map +0 -1
  40. package/dist/collections.d.mts +0 -64
  41. package/dist/collections.js.map +0 -1
  42. package/dist/collections.mjs +0 -9
  43. package/dist/collections.mjs.map +0 -1
  44. package/dist/index.d.mts +0 -233
  45. package/dist/index.js.map +0 -1
  46. package/dist/index.mjs +0 -702
  47. package/dist/index.mjs.map +0 -1
  48. package/dist/products.d.mts +0 -63
  49. package/dist/products.js.map +0 -1
  50. package/dist/products.mjs +0 -9
  51. package/dist/products.mjs.map +0 -1
  52. package/dist/store-CJVUz2Yb.d.mts +0 -608
  53. package/dist/store.d.mts +0 -1
  54. package/dist/store.js.map +0 -1
  55. package/dist/store.mjs +0 -9
  56. package/dist/store.mjs.map +0 -1
  57. package/dist/utils/rate-limit.d.mts +0 -25
  58. package/dist/utils/rate-limit.js.map +0 -1
  59. package/dist/utils/rate-limit.mjs +0 -11
  60. package/dist/utils/rate-limit.mjs.map +0 -1
@@ -1,12 +1,6 @@
1
- import {
2
- formatPrice
3
- } from "./chunk-BWKBRM2Z.mjs";
4
1
  import {
5
2
  rateLimitedFetch
6
- } from "./chunk-2KBOKOAD.mjs";
7
-
8
- // src/products.ts
9
- import { filter, isNonNullish } from "remeda";
3
+ } from "./chunk-2MF53V33.js";
10
4
 
11
5
  // src/ai/enrich.ts
12
6
  import TurndownService from "turndown";
@@ -34,7 +28,7 @@ function normalizeDomainToBase(domain) {
34
28
  async function fetchAjaxProduct(domain, handle) {
35
29
  const base = normalizeDomainToBase(domain);
36
30
  const url = `${base}/products/${handle}.js`;
37
- const res = await rateLimitedFetch(url);
31
+ const res = await rateLimitedFetch(url, { rateLimitClass: "products:ajax" });
38
32
  if (!res.ok) throw new Error(`Failed to fetch AJAX product: ${url}`);
39
33
  const data = await res.json();
40
34
  return data;
@@ -42,7 +36,7 @@ async function fetchAjaxProduct(domain, handle) {
42
36
  async function fetchProductPage(domain, handle) {
43
37
  const base = normalizeDomainToBase(domain);
44
38
  const url = `${base}/products/${handle}`;
45
- const res = await rateLimitedFetch(url);
39
+ const res = await rateLimitedFetch(url, { rateLimitClass: "products:html" });
46
40
  if (!res.ok) throw new Error(`Failed to fetch product page: ${url}`);
47
41
  return res.text();
48
42
  }
@@ -260,7 +254,7 @@ async function callOpenRouter(model, prompt, apiKey) {
260
254
  Authorization: `Bearer ${apiKey}`
261
255
  };
262
256
  const referer = process.env.OPENROUTER_SITE_URL || process.env.SITE_URL;
263
- const title = process.env.OPENROUTER_APP_TITLE || "Shop Search";
257
+ const title = process.env.OPENROUTER_APP_TITLE || "Shop Client";
264
258
  if (referer) headers["HTTP-Referer"] = referer;
265
259
  if (title) headers["X-Title"] = title;
266
260
  const buildPayload = (m) => ({
@@ -285,7 +279,8 @@ async function callOpenRouter(model, prompt, apiKey) {
285
279
  method: "POST",
286
280
  headers,
287
281
  body: JSON.stringify(buildPayload(m)),
288
- signal: controller.signal
282
+ signal: controller.signal,
283
+ rateLimitClass: "ai:openrouter"
289
284
  });
290
285
  clearTimeout(timeout);
291
286
  if (!response.ok) {
@@ -856,432 +851,15 @@ function pruneBreakdownForSignals(breakdown, text) {
856
851
  return pruned;
857
852
  }
858
853
 
859
- // src/products.ts
860
- function createProductOperations(baseUrl, storeDomain, fetchProducts, productsDto, productDto, getStoreInfo, findProduct) {
861
- function applyCurrencyOverride(product, currency) {
862
- var _a, _b, _c, _d, _e, _f;
863
- const priceMin = (_b = (_a = product.priceMin) != null ? _a : product.price) != null ? _b : 0;
864
- const priceMax = (_d = (_c = product.priceMax) != null ? _c : product.price) != null ? _d : 0;
865
- const compareAtMin = (_f = (_e = product.compareAtPriceMin) != null ? _e : product.compareAtPrice) != null ? _f : 0;
866
- return {
867
- ...product,
868
- currency,
869
- localizedPricing: {
870
- currency,
871
- priceFormatted: formatPrice(priceMin, currency),
872
- priceMinFormatted: formatPrice(priceMin, currency),
873
- priceMaxFormatted: formatPrice(priceMax, currency),
874
- compareAtPriceFormatted: formatPrice(compareAtMin, currency)
875
- }
876
- };
877
- }
878
- function maybeOverrideProductsCurrency(products, currency) {
879
- if (!products || !currency) return products;
880
- return products.map((p) => applyCurrencyOverride(p, currency));
881
- }
882
- const operations = {
883
- /**
884
- * Fetches all products from the store across all pages.
885
- *
886
- * @returns {Promise<Product[] | null>} Array of all products or null if error occurs
887
- *
888
- * @throws {Error} When there's a network error or API failure
889
- *
890
- * @example
891
- * ```typescript
892
- * const shop = new ShopClient('https://exampleshop.com');
893
- * const allProducts = await shop.products.all();
894
- *
895
- * console.log(`Found ${allProducts?.length} products`);
896
- * allProducts?.forEach(product => {
897
- * console.log(product.title, product.price);
898
- * });
899
- * ```
900
- */
901
- all: async (options) => {
902
- const limit = 250;
903
- const allProducts = [];
904
- async function fetchAll() {
905
- let currentPage = 1;
906
- while (true) {
907
- const products = await fetchProducts(currentPage, limit);
908
- if (!products || products.length === 0 || products.length < limit) {
909
- if (products && products.length > 0) {
910
- allProducts.push(...products);
911
- }
912
- break;
913
- }
914
- allProducts.push(...products);
915
- currentPage++;
916
- }
917
- return allProducts;
918
- }
919
- try {
920
- const products = await fetchAll();
921
- return maybeOverrideProductsCurrency(products, options == null ? void 0 : options.currency);
922
- } catch (error) {
923
- console.error("Failed to fetch all products:", storeDomain, error);
924
- throw error;
925
- }
926
- },
927
- /**
928
- * Fetches products with pagination support.
929
- *
930
- * @param options - Pagination options
931
- * @param options.page - Page number (default: 1)
932
- * @param options.limit - Number of products per page (default: 250, max: 250)
933
- *
934
- * @returns {Promise<Product[] | null>} Array of products for the specified page or null if error occurs
935
- *
936
- * @throws {Error} When there's a network error or API failure
937
- *
938
- * @example
939
- * ```typescript
940
- * const shop = new ShopClient('https://example.myshopify.com');
941
- *
942
- * // Get first page with default limit (250)
943
- * const firstPage = await shop.products.paginated();
944
- *
945
- * // Get second page with custom limit
946
- * const secondPage = await shop.products.paginated({ page: 2, limit: 50 });
947
- * ```
948
- */
949
- paginated: async (options) => {
950
- var _a, _b;
951
- const page = (_a = options == null ? void 0 : options.page) != null ? _a : 1;
952
- const limit = Math.min((_b = options == null ? void 0 : options.limit) != null ? _b : 250, 250);
953
- const url = `${baseUrl}products.json?limit=${limit}&page=${page}`;
954
- try {
955
- const response = await rateLimitedFetch(url);
956
- if (!response.ok) {
957
- console.error(
958
- `HTTP error! status: ${response.status} for ${storeDomain} page ${page}`
959
- );
960
- throw new Error(`HTTP error! status: ${response.status}`);
961
- }
962
- const data = await response.json();
963
- if (data.products.length === 0) {
964
- return [];
965
- }
966
- const normalized = productsDto(data.products);
967
- return maybeOverrideProductsCurrency(normalized, options == null ? void 0 : options.currency);
968
- } catch (error) {
969
- console.error(
970
- `Error fetching products for ${storeDomain} page ${page} with limit ${limit}:`,
971
- error
972
- );
973
- return null;
974
- }
975
- },
976
- /**
977
- * Finds a specific product by its handle.
978
- *
979
- * @param productHandle - The product handle (URL slug) to search for
980
- *
981
- * @returns {Promise<Product | null>} The product if found, null if not found
982
- *
983
- * @throws {Error} When the handle is invalid or there's a network error
984
- *
985
- * @example
986
- * ```typescript
987
- * const shop = new ShopClient('https://exampleshop.com');
988
- *
989
- * // Find product by handle
990
- * const product = await shop.products.find('awesome-t-shirt');
991
- *
992
- * if (product) {
993
- * console.log(product.title, product.price);
994
- * console.log('Available variants:', product.variants.length);
995
- * }
996
- *
997
- * // Handle with query string
998
- * const productWithVariant = await shop.products.find('t-shirt?variant=123');
999
- * ```
1000
- */
1001
- find: async (productHandle, options) => {
1002
- var _a, _b;
1003
- if (!productHandle || typeof productHandle !== "string") {
1004
- throw new Error("Product handle is required and must be a string");
1005
- }
1006
- try {
1007
- let qs = null;
1008
- if (productHandle.includes("?")) {
1009
- const parts = productHandle.split("?");
1010
- const handlePart = (_a = parts[0]) != null ? _a : productHandle;
1011
- const qsPart = (_b = parts[1]) != null ? _b : null;
1012
- productHandle = handlePart;
1013
- qs = qsPart;
1014
- }
1015
- const sanitizedHandle = productHandle.trim().replace(/[^a-zA-Z0-9\-_]/g, "");
1016
- if (!sanitizedHandle) {
1017
- throw new Error("Invalid product handle format");
1018
- }
1019
- if (sanitizedHandle.length > 255) {
1020
- throw new Error("Product handle is too long");
1021
- }
1022
- let finalHandle = sanitizedHandle;
1023
- try {
1024
- const htmlResp = await rateLimitedFetch(
1025
- `${baseUrl}products/${encodeURIComponent(sanitizedHandle)}`
1026
- );
1027
- if (htmlResp.ok) {
1028
- const finalUrl = htmlResp.url;
1029
- if (finalUrl) {
1030
- const pathname = new URL(finalUrl).pathname.replace(/\/$/, "");
1031
- const parts = pathname.split("/").filter(Boolean);
1032
- const idx = parts.indexOf("products");
1033
- const maybeHandle = idx >= 0 ? parts[idx + 1] : void 0;
1034
- if (typeof maybeHandle === "string" && maybeHandle.length) {
1035
- finalHandle = maybeHandle;
1036
- }
1037
- }
1038
- }
1039
- } catch {
1040
- }
1041
- const url = `${baseUrl}products/${encodeURIComponent(finalHandle)}.js${qs ? `?${qs}` : ""}`;
1042
- const response = await rateLimitedFetch(url);
1043
- if (!response.ok) {
1044
- if (response.status === 404) {
1045
- return null;
1046
- }
1047
- throw new Error(`HTTP error! status: ${response.status}`);
1048
- }
1049
- const product = await response.json();
1050
- const productData = productDto(product);
1051
- return (options == null ? void 0 : options.currency) ? applyCurrencyOverride(productData, options.currency) : productData;
1052
- } catch (error) {
1053
- if (error instanceof Error) {
1054
- console.error(
1055
- `Error fetching product ${productHandle}:`,
1056
- baseUrl,
1057
- error.message
1058
- );
1059
- }
1060
- throw error;
1061
- }
1062
- },
1063
- /**
1064
- * Enrich a product by generating merged markdown from body_html and product page.
1065
- * Adds `enriched_content` to the returned product.
1066
- */
1067
- enriched: async (productHandle, options) => {
1068
- if (!productHandle || typeof productHandle !== "string") {
1069
- throw new Error("Product handle is required and must be a string");
1070
- }
1071
- const apiKey = (options == null ? void 0 : options.apiKey) || process.env.OPENROUTER_API_KEY;
1072
- if (!apiKey) {
1073
- throw new Error(
1074
- "Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY."
1075
- );
1076
- }
1077
- const baseProduct = await operations.find(productHandle);
1078
- if (!baseProduct) {
1079
- return null;
1080
- }
1081
- const handle = baseProduct.handle;
1082
- const enriched = await enrichProduct(storeDomain, handle, {
1083
- apiKey,
1084
- useGfm: options == null ? void 0 : options.useGfm,
1085
- inputType: options == null ? void 0 : options.inputType,
1086
- model: options == null ? void 0 : options.model,
1087
- outputFormat: options == null ? void 0 : options.outputFormat
1088
- });
1089
- return {
1090
- ...baseProduct,
1091
- enriched_content: enriched.mergedMarkdown
1092
- };
1093
- },
1094
- classify: async (productHandle, options) => {
1095
- if (!productHandle || typeof productHandle !== "string") {
1096
- throw new Error("Product handle is required and must be a string");
1097
- }
1098
- const apiKey = (options == null ? void 0 : options.apiKey) || process.env.OPENROUTER_API_KEY;
1099
- if (!apiKey) {
1100
- throw new Error(
1101
- "Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY."
1102
- );
1103
- }
1104
- const enrichedProduct = await operations.enriched(productHandle, {
1105
- apiKey,
1106
- inputType: "html",
1107
- model: options == null ? void 0 : options.model,
1108
- outputFormat: "json"
1109
- });
1110
- if (!enrichedProduct || !enrichedProduct.enriched_content) return null;
1111
- let productContent = enrichedProduct.enriched_content;
1112
- try {
1113
- const obj = JSON.parse(enrichedProduct.enriched_content);
1114
- const lines = [];
1115
- if (obj.title && typeof obj.title === "string")
1116
- lines.push(`Title: ${obj.title}`);
1117
- if (obj.description && typeof obj.description === "string")
1118
- lines.push(`Description: ${obj.description}`);
1119
- if (Array.isArray(obj.materials) && obj.materials.length)
1120
- lines.push(`Materials: ${obj.materials.join(", ")}`);
1121
- if (Array.isArray(obj.care) && obj.care.length)
1122
- lines.push(`Care: ${obj.care.join(", ")}`);
1123
- if (obj.fit && typeof obj.fit === "string")
1124
- lines.push(`Fit: ${obj.fit}`);
1125
- if (obj.returnPolicy && typeof obj.returnPolicy === "string")
1126
- lines.push(`ReturnPolicy: ${obj.returnPolicy}`);
1127
- productContent = lines.join("\n");
1128
- } catch {
1129
- }
1130
- const classification = await classifyProduct(productContent, {
1131
- apiKey,
1132
- model: options == null ? void 0 : options.model
1133
- });
1134
- return classification;
1135
- },
1136
- generateSEOContent: async (productHandle, options) => {
1137
- if (!productHandle || typeof productHandle !== "string") {
1138
- throw new Error("Product handle is required and must be a string");
1139
- }
1140
- const apiKey = (options == null ? void 0 : options.apiKey) || process.env.OPENROUTER_API_KEY;
1141
- if (!apiKey) {
1142
- throw new Error(
1143
- "Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY."
1144
- );
1145
- }
1146
- const baseProduct = await operations.find(productHandle);
1147
- if (!baseProduct) return null;
1148
- const payload = {
1149
- title: baseProduct.title,
1150
- description: baseProduct.bodyHtml || void 0,
1151
- vendor: baseProduct.vendor,
1152
- price: baseProduct.price,
1153
- tags: baseProduct.tags
1154
- };
1155
- const seo = await generateSEOContent(payload, {
1156
- apiKey,
1157
- model: options == null ? void 0 : options.model
1158
- });
1159
- return seo;
1160
- },
1161
- /**
1162
- * Fetches products that are showcased/featured on the store's homepage.
1163
- *
1164
- * @returns {Promise<Product[]>} Array of showcased products found on the homepage
1165
- *
1166
- * @throws {Error} When there's a network error or API failure
1167
- *
1168
- * @example
1169
- * ```typescript
1170
- * const shop = new ShopClient('https://exampleshop.com');
1171
- * const showcasedProducts = await shop.products.showcased();
1172
- *
1173
- * console.log(`Found ${showcasedProducts.length} showcased products`);
1174
- * showcasedProducts.forEach(product => {
1175
- * console.log(`Featured: ${product.title} - ${product.price}`);
1176
- * });
1177
- * ```
1178
- */
1179
- showcased: async () => {
1180
- const storeInfo = await getStoreInfo();
1181
- const products = await Promise.all(
1182
- storeInfo.showcase.products.map(
1183
- (productHandle) => findProduct(productHandle)
1184
- )
1185
- );
1186
- return filter(products, isNonNullish);
1187
- },
1188
- /**
1189
- * Creates a filter map of variant options and their distinct values from all products.
1190
- *
1191
- * @returns {Promise<Record<string, string[]> | null>} Map of option names to their distinct values or null if error occurs
1192
- *
1193
- * @throws {Error} When there's a network error or API failure
1194
- *
1195
- * @example
1196
- * ```typescript
1197
- * const shop = new ShopClient('https://exampleshop.com');
1198
- * const filters = await shop.products.filter();
1199
- *
1200
- * console.log('Available filters:', filters);
1201
- * // Output: { "Size": ["S", "M", "L", "XL"], "Color": ["Red", "Blue", "Green"] }
1202
- *
1203
- * // Use filters for UI components
1204
- * Object.entries(filters || {}).forEach(([optionName, values]) => {
1205
- * console.log(`${optionName}: ${values.join(', ')}`);
1206
- * });
1207
- * ```
1208
- */
1209
- filter: async () => {
1210
- try {
1211
- const products = await operations.all();
1212
- if (!products || products.length === 0) {
1213
- return {};
1214
- }
1215
- const filterMap = {};
1216
- products.forEach((product) => {
1217
- if (product.variants && product.variants.length > 0) {
1218
- if (product.options && product.options.length > 0) {
1219
- product.options.forEach((option) => {
1220
- const lowercaseOptionName = option.name.toLowerCase();
1221
- if (!filterMap[lowercaseOptionName]) {
1222
- filterMap[lowercaseOptionName] = /* @__PURE__ */ new Set();
1223
- }
1224
- option.values.forEach((value) => {
1225
- const trimmed = value == null ? void 0 : value.trim();
1226
- if (trimmed) {
1227
- let set = filterMap[lowercaseOptionName];
1228
- if (!set) {
1229
- set = /* @__PURE__ */ new Set();
1230
- filterMap[lowercaseOptionName] = set;
1231
- }
1232
- set.add(trimmed.toLowerCase());
1233
- }
1234
- });
1235
- });
1236
- }
1237
- product.variants.forEach((variant) => {
1238
- var _a, _b, _c, _d, _e, _f;
1239
- if (variant.option1) {
1240
- const optionName = (((_b = (_a = product.options) == null ? void 0 : _a[0]) == null ? void 0 : _b.name) || "Option 1").toLowerCase();
1241
- let set1 = filterMap[optionName];
1242
- if (!set1) {
1243
- set1 = /* @__PURE__ */ new Set();
1244
- filterMap[optionName] = set1;
1245
- }
1246
- set1.add(variant.option1.trim().toLowerCase());
1247
- }
1248
- if (variant.option2) {
1249
- const optionName = (((_d = (_c = product.options) == null ? void 0 : _c[1]) == null ? void 0 : _d.name) || "Option 2").toLowerCase();
1250
- let set2 = filterMap[optionName];
1251
- if (!set2) {
1252
- set2 = /* @__PURE__ */ new Set();
1253
- filterMap[optionName] = set2;
1254
- }
1255
- set2.add(variant.option2.trim().toLowerCase());
1256
- }
1257
- if (variant.option3) {
1258
- const optionName = (((_f = (_e = product.options) == null ? void 0 : _e[2]) == null ? void 0 : _f.name) || "Option 3").toLowerCase();
1259
- if (!filterMap[optionName]) {
1260
- filterMap[optionName] = /* @__PURE__ */ new Set();
1261
- }
1262
- filterMap[optionName].add(variant.option3.trim().toLowerCase());
1263
- }
1264
- });
1265
- }
1266
- });
1267
- const result = {};
1268
- Object.entries(filterMap).forEach(([optionName, valueSet]) => {
1269
- result[optionName] = Array.from(valueSet).sort();
1270
- });
1271
- return result;
1272
- } catch (error) {
1273
- console.error("Failed to create product filters:", storeDomain, error);
1274
- throw error;
1275
- }
1276
- }
1277
- };
1278
- return operations;
1279
- }
1280
-
1281
854
  export {
855
+ fetchAjaxProduct,
856
+ fetchProductPage,
857
+ extractMainSection,
858
+ htmlToMarkdown,
859
+ mergeWithLLM,
860
+ enrichProduct,
1282
861
  classifyProduct,
1283
862
  generateSEOContent,
1284
863
  determineStoreType,
1285
- createProductOperations
864
+ pruneBreakdownForSignals
1286
865
  };
1287
- //# sourceMappingURL=chunk-WTK5HUFI.mjs.map
@@ -1,4 +1,5 @@
1
- import { f as Collection, y as CurrencyCode, c as Product, e as ShopifyCollection, g as StoreInfo } from './store-CJVUz2Yb.js';
1
+ import { StoreInfo } from './store.js';
2
+ import { C as Collection, f as CurrencyCode, P as Product, b as ShopifyCollection } from './types-luPg5O08.js';
2
3
 
3
4
  /**
4
5
  * Interface for collection operations