shop-client 3.9.0 → 3.9.2

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 (41) hide show
  1. package/README.md +65 -0
  2. package/dist/ai/enrich.d.ts +93 -0
  3. package/dist/ai/enrich.js +25 -0
  4. package/dist/checkout.js +6 -0
  5. package/dist/chunk-2MF53V33.js +196 -0
  6. package/dist/chunk-CN7L3BHG.js +147 -0
  7. package/dist/chunk-CXUCPK6X.js +460 -0
  8. package/dist/chunk-DJQEZNHG.js +233 -0
  9. package/dist/chunk-MOBWPEY4.js +420 -0
  10. package/dist/chunk-QUDGES3A.js +195 -0
  11. package/dist/chunk-RR6YTQWP.js +90 -0
  12. package/dist/chunk-VPPCOJC3.js +865 -0
  13. package/dist/collections.d.ts +2 -1
  14. package/dist/collections.js +8 -0
  15. package/dist/index.d.ts +7 -84
  16. package/dist/index.js +753 -0
  17. package/dist/products.d.ts +2 -1
  18. package/dist/products.js +8 -0
  19. package/dist/store.d.ts +53 -1
  20. package/dist/store.js +9 -0
  21. package/dist/{store-iQARl6J3.d.ts → types-luPg5O08.d.ts} +1 -208
  22. package/dist/utils/detect-country.d.ts +32 -0
  23. package/dist/utils/detect-country.js +6 -0
  24. package/dist/utils/func.d.ts +61 -0
  25. package/dist/utils/func.js +24 -0
  26. package/dist/utils/rate-limit.js +10 -0
  27. package/package.json +16 -3
  28. package/dist/checkout.mjs +0 -1
  29. package/dist/chunk-6GPWNCDO.mjs +0 -130
  30. package/dist/chunk-EJO5U4BT.mjs +0 -2
  31. package/dist/chunk-FFKWCNLU.mjs +0 -1
  32. package/dist/chunk-KYLPIEU3.mjs +0 -2
  33. package/dist/chunk-MB2INNNP.mjs +0 -1
  34. package/dist/chunk-MI7754VX.mjs +0 -2
  35. package/dist/chunk-SZQPMLZG.mjs +0 -1
  36. package/dist/collections.mjs +0 -1
  37. package/dist/enrich-OZHBXKK6.mjs +0 -1
  38. package/dist/index.mjs +0 -2
  39. package/dist/products.mjs +0 -1
  40. package/dist/store.mjs +0 -1
  41. package/dist/utils/rate-limit.mjs +0 -1
package/dist/index.js ADDED
@@ -0,0 +1,753 @@
1
+ import {
2
+ createCheckoutOperations
3
+ } from "./chunk-RR6YTQWP.js";
4
+ import {
5
+ createCollectionOperations
6
+ } from "./chunk-MOBWPEY4.js";
7
+ import {
8
+ createProductOperations
9
+ } from "./chunk-CXUCPK6X.js";
10
+ import {
11
+ createStoreOperations,
12
+ getInfoForStore
13
+ } from "./chunk-DJQEZNHG.js";
14
+ import {
15
+ classifyProduct,
16
+ determineStoreType,
17
+ generateSEOContent
18
+ } from "./chunk-VPPCOJC3.js";
19
+ import {
20
+ configureRateLimit,
21
+ rateLimitedFetch
22
+ } from "./chunk-2MF53V33.js";
23
+ import {
24
+ detectShopCountry
25
+ } from "./chunk-QUDGES3A.js";
26
+ import {
27
+ buildVariantOptionsMap,
28
+ calculateDiscount,
29
+ extractDomainWithoutSuffix,
30
+ genProductSlug,
31
+ generateStoreSlug,
32
+ normalizeKey,
33
+ safeParseDate,
34
+ sanitizeDomain
35
+ } from "./chunk-CN7L3BHG.js";
36
+
37
+ // src/ai/determine-store-type.ts
38
+ async function determineStoreTypeForStore(args) {
39
+ var _a, _b, _c;
40
+ const info = await args.getInfo();
41
+ const maxProducts = Math.max(0, Math.min(50, (_a = args.maxShowcaseProducts) != null ? _a : 10));
42
+ const maxCollections = Math.max(
43
+ 0,
44
+ Math.min(50, (_b = args.maxShowcaseCollections) != null ? _b : 10)
45
+ );
46
+ const take = (arr, n) => arr.slice(0, Math.max(0, n));
47
+ const productsSample = Array.isArray(info.showcase.products) ? take(info.showcase.products, maxProducts) : [];
48
+ const collectionsSample = Array.isArray(info.showcase.collections) ? take(info.showcase.collections, maxCollections) : [];
49
+ const breakdown = await determineStoreType(
50
+ {
51
+ title: info.title || info.name,
52
+ description: (_c = info.description) != null ? _c : null,
53
+ showcase: {
54
+ products: productsSample,
55
+ collections: collectionsSample
56
+ }
57
+ },
58
+ { apiKey: args.apiKey, model: args.model }
59
+ );
60
+ return breakdown;
61
+ }
62
+
63
+ // src/dto/collections.dto.ts
64
+ function collectionsDto(collections) {
65
+ if (!collections || collections.length === 0) return null;
66
+ return collections.map((collection) => ({
67
+ id: collection.id.toString(),
68
+ title: collection.title,
69
+ handle: collection.handle,
70
+ description: collection.description,
71
+ image: collection.image ? {
72
+ id: collection.image.id,
73
+ createdAt: collection.image.created_at,
74
+ src: collection.image.src,
75
+ alt: collection.image.alt
76
+ } : void 0,
77
+ productsCount: collection.products_count,
78
+ publishedAt: collection.published_at,
79
+ updatedAt: collection.updated_at
80
+ }));
81
+ }
82
+
83
+ // src/dto/products.mapped.ts
84
+ function mapVariants(product) {
85
+ var _a;
86
+ const variants = (_a = product.variants) != null ? _a : [];
87
+ return variants.map((variant) => {
88
+ var _a2;
89
+ return {
90
+ id: variant.id.toString(),
91
+ platformId: variant.id.toString(),
92
+ name: variant.name,
93
+ title: variant.title,
94
+ option1: variant.option1 || null,
95
+ option2: variant.option2 || null,
96
+ option3: variant.option3 || null,
97
+ options: [variant.option1, variant.option2, variant.option3].filter(
98
+ Boolean
99
+ ),
100
+ sku: variant.sku || null,
101
+ requiresShipping: variant.requires_shipping,
102
+ taxable: variant.taxable,
103
+ featuredImage: variant.featured_image ? {
104
+ id: variant.featured_image.id,
105
+ src: variant.featured_image.src,
106
+ width: variant.featured_image.width,
107
+ height: variant.featured_image.height,
108
+ position: variant.featured_image.position,
109
+ productId: variant.featured_image.product_id,
110
+ aspectRatio: variant.featured_image.aspect_ratio || 0,
111
+ variantIds: variant.featured_image.variant_ids || [],
112
+ createdAt: variant.featured_image.created_at,
113
+ updatedAt: variant.featured_image.updated_at,
114
+ alt: variant.featured_image.alt
115
+ } : null,
116
+ available: Boolean(variant.available),
117
+ price: typeof variant.price === "string" ? Number.parseFloat(variant.price) * 100 : variant.price,
118
+ weightInGrams: (_a2 = variant.weightInGrams) != null ? _a2 : variant.grams,
119
+ compareAtPrice: variant.compare_at_price ? typeof variant.compare_at_price === "string" ? Number.parseFloat(variant.compare_at_price) * 100 : variant.compare_at_price : 0,
120
+ position: variant.position,
121
+ productId: variant.product_id,
122
+ createdAt: variant.created_at,
123
+ updatedAt: variant.updated_at
124
+ };
125
+ });
126
+ }
127
+ function mapProductsDto(products, ctx) {
128
+ if (!products || products.length === 0) return null;
129
+ return products.map((product) => {
130
+ var _a, _b, _c;
131
+ const optionNames = product.options.map((o) => o.name);
132
+ const variantOptionsMap = buildVariantOptionsMap(
133
+ optionNames,
134
+ product.variants
135
+ );
136
+ const mappedVariants = mapVariants(product);
137
+ const priceValues = mappedVariants.map((v) => v.price).filter((p) => typeof p === "number" && !Number.isNaN(p));
138
+ const compareAtValues = mappedVariants.map((v) => v.compareAtPrice || 0).filter((p) => typeof p === "number" && !Number.isNaN(p));
139
+ const priceMin = priceValues.length ? Math.min(...priceValues) : 0;
140
+ const priceMax = priceValues.length ? Math.max(...priceValues) : 0;
141
+ const priceVaries = mappedVariants.length > 1 && priceMin !== priceMax;
142
+ const compareAtMin = compareAtValues.length ? Math.min(...compareAtValues) : 0;
143
+ const compareAtMax = compareAtValues.length ? Math.max(...compareAtValues) : 0;
144
+ const compareAtVaries = mappedVariants.length > 1 && compareAtMin !== compareAtMax;
145
+ return {
146
+ slug: genProductSlug({
147
+ handle: product.handle,
148
+ storeDomain: ctx.storeDomain
149
+ }),
150
+ handle: product.handle,
151
+ platformId: product.id.toString(),
152
+ title: product.title,
153
+ available: mappedVariants.some((v) => v.available),
154
+ price: priceMin,
155
+ priceMin,
156
+ priceMax,
157
+ priceVaries,
158
+ compareAtPrice: compareAtMin,
159
+ compareAtPriceMin: compareAtMin,
160
+ compareAtPriceMax: compareAtMax,
161
+ compareAtPriceVaries: compareAtVaries,
162
+ discount: 0,
163
+ currency: ctx.currency,
164
+ localizedPricing: {
165
+ currency: ctx.currency,
166
+ priceFormatted: ctx.formatPrice(priceMin),
167
+ priceMinFormatted: ctx.formatPrice(priceMin),
168
+ priceMaxFormatted: ctx.formatPrice(priceMax),
169
+ compareAtPriceFormatted: ctx.formatPrice(compareAtMin)
170
+ },
171
+ options: product.options.map((option) => ({
172
+ key: normalizeKey(option.name),
173
+ data: option.values,
174
+ name: option.name,
175
+ position: option.position,
176
+ values: option.values
177
+ })),
178
+ variantOptionsMap,
179
+ bodyHtml: product.body_html || null,
180
+ active: true,
181
+ productType: product.product_type || null,
182
+ tags: Array.isArray(product.tags) ? product.tags : [],
183
+ vendor: product.vendor,
184
+ featuredImage: ((_b = (_a = product.images) == null ? void 0 : _a[0]) == null ? void 0 : _b.src) ? ctx.normalizeImageUrl(product.images[0].src) : null,
185
+ isProxyFeaturedImage: false,
186
+ createdAt: safeParseDate(product.created_at),
187
+ updatedAt: safeParseDate(product.updated_at),
188
+ variants: mappedVariants,
189
+ images: product.images.map((image) => ({
190
+ id: image.id,
191
+ productId: image.product_id,
192
+ alt: null,
193
+ position: image.position,
194
+ src: ctx.normalizeImageUrl(image.src),
195
+ width: image.width,
196
+ height: image.height,
197
+ mediaType: "image",
198
+ variantIds: image.variant_ids || [],
199
+ createdAt: image.created_at,
200
+ updatedAt: image.updated_at
201
+ })),
202
+ publishedAt: (_c = safeParseDate(product.published_at)) != null ? _c : null,
203
+ seo: null,
204
+ metaTags: null,
205
+ displayScore: void 0,
206
+ deletedAt: null,
207
+ storeSlug: ctx.storeSlug,
208
+ storeDomain: ctx.storeDomain,
209
+ url: `${ctx.storeDomain}/products/${product.handle}`
210
+ };
211
+ });
212
+ }
213
+ function mapProductDto(product, ctx) {
214
+ var _a;
215
+ const optionNames = product.options.map((o) => o.name);
216
+ const variantOptionsMap = buildVariantOptionsMap(
217
+ optionNames,
218
+ product.variants
219
+ );
220
+ const mapped = {
221
+ slug: genProductSlug({
222
+ handle: product.handle,
223
+ storeDomain: ctx.storeDomain
224
+ }),
225
+ handle: product.handle,
226
+ platformId: product.id.toString(),
227
+ title: product.title,
228
+ available: product.available,
229
+ price: product.price,
230
+ priceMin: product.price_min,
231
+ priceMax: product.price_max,
232
+ priceVaries: product.price_varies,
233
+ compareAtPrice: product.compare_at_price || 0,
234
+ compareAtPriceMin: product.compare_at_price_min,
235
+ compareAtPriceMax: product.compare_at_price_max,
236
+ compareAtPriceVaries: product.compare_at_price_varies,
237
+ discount: 0,
238
+ currency: ctx.currency,
239
+ localizedPricing: {
240
+ currency: ctx.currency,
241
+ priceFormatted: ctx.formatPrice(product.price),
242
+ priceMinFormatted: ctx.formatPrice(product.price_min),
243
+ priceMaxFormatted: ctx.formatPrice(product.price_max),
244
+ compareAtPriceFormatted: ctx.formatPrice(product.compare_at_price || 0)
245
+ },
246
+ options: product.options.map((option) => ({
247
+ key: normalizeKey(option.name),
248
+ data: option.values,
249
+ name: option.name,
250
+ position: option.position,
251
+ values: option.values
252
+ })),
253
+ variantOptionsMap,
254
+ bodyHtml: product.description || null,
255
+ active: true,
256
+ productType: product.type || null,
257
+ tags: Array.isArray(product.tags) ? product.tags : typeof product.tags === "string" ? [product.tags] : [],
258
+ vendor: product.vendor,
259
+ featuredImage: ctx.normalizeImageUrl(product.featured_image),
260
+ isProxyFeaturedImage: false,
261
+ createdAt: safeParseDate(product.created_at),
262
+ updatedAt: safeParseDate(product.updated_at),
263
+ variants: mapVariants(product),
264
+ images: Array.isArray(product.images) ? product.images.map((imageSrc, index) => ({
265
+ id: index + 1,
266
+ productId: product.id,
267
+ alt: null,
268
+ position: index + 1,
269
+ src: ctx.normalizeImageUrl(imageSrc),
270
+ width: 0,
271
+ height: 0,
272
+ mediaType: "image",
273
+ variantIds: [],
274
+ createdAt: product.created_at,
275
+ updatedAt: product.updated_at
276
+ })) : [],
277
+ publishedAt: (_a = safeParseDate(product.published_at)) != null ? _a : null,
278
+ seo: null,
279
+ metaTags: null,
280
+ displayScore: void 0,
281
+ deletedAt: null,
282
+ storeSlug: ctx.storeSlug,
283
+ storeDomain: ctx.storeDomain,
284
+ url: product.url || `${ctx.storeDomain}/products/${product.handle}`
285
+ };
286
+ return mapped;
287
+ }
288
+
289
+ // src/index.ts
290
+ var ShopClient = class {
291
+ /**
292
+ * Creates a new ShopClient instance for interacting with a Shopify store.
293
+ *
294
+ * @param urlPath - The Shopify store URL (e.g., 'https://exampleshop.com' or 'exampleshop.com')
295
+ *
296
+ * @throws {Error} When the URL is invalid or contains malicious patterns
297
+ *
298
+ * @example
299
+ * ```typescript
300
+ * // With full URL
301
+ * const shop = new ShopClient('https://exampleshop.com');
302
+ *
303
+ * // Without protocol (automatically adds https://)
304
+ * const shop = new ShopClient('exampleshop.com');
305
+ *
306
+ * // Works with any Shopify store domain
307
+ * const shop1 = new ShopClient('https://example.myshopify.com');
308
+ * const shop2 = new ShopClient('https://boutique.fashion');
309
+ * ```
310
+ */
311
+ constructor(urlPath, options) {
312
+ this.validationCache = /* @__PURE__ */ new Map();
313
+ this.cacheExpiry = 5 * 60 * 1e3;
314
+ // 5 minutes
315
+ this.cacheTimestamps = /* @__PURE__ */ new Map();
316
+ this.normalizeImageUrlCache = /* @__PURE__ */ new Map();
317
+ if (!urlPath || typeof urlPath !== "string") {
318
+ throw new Error("Store URL is required and must be a string");
319
+ }
320
+ let normalizedUrl = urlPath.trim();
321
+ if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
322
+ normalizedUrl = `https://${normalizedUrl}`;
323
+ }
324
+ let storeUrl;
325
+ try {
326
+ storeUrl = new URL(normalizedUrl);
327
+ } catch (_error) {
328
+ throw new Error("Invalid store URL format");
329
+ }
330
+ const hostname = storeUrl.hostname;
331
+ if (!hostname || hostname.length < 3) {
332
+ throw new Error("Invalid domain name");
333
+ }
334
+ if (hostname.includes("..") || hostname.includes("//") || hostname.includes("@")) {
335
+ throw new Error("Invalid characters in domain name");
336
+ }
337
+ if (!hostname.includes(".") || hostname.startsWith(".") || hostname.endsWith(".")) {
338
+ throw new Error(
339
+ "Invalid domain format - must be a valid domain with TLD"
340
+ );
341
+ }
342
+ const domainPattern = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
343
+ if (!domainPattern.test(hostname)) {
344
+ throw new Error("Invalid domain format");
345
+ }
346
+ this.storeDomain = `https://${hostname}`;
347
+ let fetchUrl = `https://${hostname}${storeUrl.pathname}`;
348
+ if (!fetchUrl.endsWith("/")) {
349
+ fetchUrl = `${fetchUrl}/`;
350
+ }
351
+ this.baseUrl = fetchUrl;
352
+ this.storeSlug = generateStoreSlug(this.storeDomain);
353
+ if (typeof (options == null ? void 0 : options.cacheTTL) === "number" && options.cacheTTL > 0) {
354
+ this.cacheExpiry = options.cacheTTL;
355
+ }
356
+ this.storeOperations = createStoreOperations({
357
+ baseUrl: this.baseUrl,
358
+ storeDomain: this.storeDomain,
359
+ validateProductExists: this.validateProductExists.bind(this),
360
+ validateCollectionExists: this.validateCollectionExists.bind(this),
361
+ validateLinksInBatches: this.validateLinksInBatches.bind(this),
362
+ handleFetchError: this.handleFetchError.bind(this)
363
+ });
364
+ this.products = createProductOperations(
365
+ this.baseUrl,
366
+ this.storeDomain,
367
+ this.fetchProducts.bind(this),
368
+ this.productsDto.bind(this),
369
+ this.productDto.bind(this),
370
+ () => this.getInfo(),
371
+ (handle) => this.products.find(handle)
372
+ );
373
+ this.collections = createCollectionOperations(
374
+ this.baseUrl,
375
+ this.storeDomain,
376
+ this.fetchCollections.bind(this),
377
+ this.collectionsDto.bind(this),
378
+ this.fetchPaginatedProductsFromCollection.bind(this),
379
+ () => this.getInfo(),
380
+ (handle) => this.collections.find(handle)
381
+ );
382
+ this.checkout = createCheckoutOperations(this.baseUrl);
383
+ }
384
+ /**
385
+ * Optimized image URL normalization with caching
386
+ */
387
+ normalizeImageUrl(url) {
388
+ if (!url) {
389
+ return "";
390
+ }
391
+ if (this.normalizeImageUrlCache.has(url)) {
392
+ return this.normalizeImageUrlCache.get(url);
393
+ }
394
+ const normalized = url.startsWith("//") ? `https:${url}` : url;
395
+ this.normalizeImageUrlCache.set(url, normalized);
396
+ return normalized;
397
+ }
398
+ /**
399
+ * Format a price amount (in cents) using the store currency.
400
+ */
401
+ formatPrice(amountInCents) {
402
+ var _a;
403
+ const currency = (_a = this.storeCurrency) != null ? _a : "USD";
404
+ try {
405
+ return new Intl.NumberFormat(void 0, {
406
+ style: "currency",
407
+ currency
408
+ }).format((amountInCents || 0) / 100);
409
+ } catch {
410
+ const val = (amountInCents || 0) / 100;
411
+ return `${val} ${currency}`;
412
+ }
413
+ }
414
+ /**
415
+ * Transform Shopify products to our Product format
416
+ */
417
+ productsDto(products) {
418
+ var _a;
419
+ return mapProductsDto(products, {
420
+ storeDomain: this.storeDomain,
421
+ storeSlug: this.storeSlug,
422
+ currency: (_a = this.storeCurrency) != null ? _a : "USD",
423
+ normalizeImageUrl: (url) => this.normalizeImageUrl(url),
424
+ formatPrice: (amount) => this.formatPrice(amount)
425
+ });
426
+ }
427
+ productDto(product) {
428
+ var _a;
429
+ return mapProductDto(product, {
430
+ storeDomain: this.storeDomain,
431
+ storeSlug: this.storeSlug,
432
+ currency: (_a = this.storeCurrency) != null ? _a : "USD",
433
+ normalizeImageUrl: (url) => this.normalizeImageUrl(url),
434
+ formatPrice: (amount) => this.formatPrice(amount)
435
+ });
436
+ }
437
+ collectionsDto(collections) {
438
+ var _a;
439
+ return (_a = collectionsDto(collections)) != null ? _a : [];
440
+ }
441
+ /**
442
+ * Enhanced error handling with context
443
+ */
444
+ handleFetchError(error, context, url) {
445
+ let errorMessage = `Error ${context}`;
446
+ let statusCode;
447
+ if (error instanceof Error) {
448
+ errorMessage += `: ${error.message}`;
449
+ const anyErr = error;
450
+ if (anyErr && typeof anyErr.status === "number") {
451
+ statusCode = anyErr.status;
452
+ }
453
+ } else if (typeof error === "string") {
454
+ errorMessage += `: ${error}`;
455
+ } else {
456
+ errorMessage += ": Unknown error occurred";
457
+ }
458
+ errorMessage += ` (URL: ${url})`;
459
+ if (statusCode) {
460
+ errorMessage += ` (Status: ${statusCode})`;
461
+ }
462
+ const enhancedError = new Error(errorMessage);
463
+ enhancedError.context = context;
464
+ enhancedError.url = url;
465
+ enhancedError.statusCode = statusCode;
466
+ enhancedError.originalError = error;
467
+ throw enhancedError;
468
+ }
469
+ /**
470
+ * Fetch products with pagination
471
+ */
472
+ async fetchProducts(page, limit) {
473
+ try {
474
+ const url = `${this.baseUrl}products.json?page=${page}&limit=${limit}`;
475
+ const response = await rateLimitedFetch(url, {
476
+ rateLimitClass: "products:list"
477
+ });
478
+ if (!response.ok) {
479
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
480
+ }
481
+ const data = await response.json();
482
+ return this.productsDto(data.products);
483
+ } catch (error) {
484
+ this.handleFetchError(
485
+ error,
486
+ "fetching products",
487
+ `${this.baseUrl}products.json`
488
+ );
489
+ }
490
+ }
491
+ /**
492
+ * Fetch collections with pagination
493
+ */
494
+ async fetchCollections(page, limit) {
495
+ try {
496
+ const url = `${this.baseUrl}collections.json?page=${page}&limit=${limit}`;
497
+ const response = await rateLimitedFetch(url, {
498
+ rateLimitClass: "collections:list"
499
+ });
500
+ if (!response.ok) {
501
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
502
+ }
503
+ const data = await response.json();
504
+ return this.collectionsDto(data.collections);
505
+ } catch (error) {
506
+ this.handleFetchError(
507
+ error,
508
+ "fetching collections",
509
+ `${this.baseUrl}collections.json`
510
+ );
511
+ }
512
+ }
513
+ /**
514
+ * Fetch paginated products from a specific collection
515
+ */
516
+ async fetchPaginatedProductsFromCollection(collectionHandle, options = {}) {
517
+ try {
518
+ const { page = 1, limit = 250 } = options;
519
+ let finalHandle = collectionHandle;
520
+ try {
521
+ const htmlResp = await rateLimitedFetch(
522
+ `${this.baseUrl}collections/${encodeURIComponent(collectionHandle)}`,
523
+ { rateLimitClass: "collections:resolve" }
524
+ );
525
+ if (htmlResp.ok) {
526
+ const finalUrl = htmlResp.url;
527
+ if (finalUrl) {
528
+ const pathname = new URL(finalUrl).pathname.replace(/\/$/, "");
529
+ const parts = pathname.split("/").filter(Boolean);
530
+ const idx = parts.indexOf("collections");
531
+ const maybeHandle = idx >= 0 ? parts[idx + 1] : void 0;
532
+ if (typeof maybeHandle === "string" && maybeHandle.length) {
533
+ finalHandle = maybeHandle;
534
+ }
535
+ }
536
+ }
537
+ } catch {
538
+ }
539
+ const url = `${this.baseUrl}collections/${finalHandle}/products.json?page=${page}&limit=${limit}`;
540
+ const response = await rateLimitedFetch(url, {
541
+ rateLimitClass: "collections:items"
542
+ });
543
+ if (!response.ok) {
544
+ if (response.status === 404) {
545
+ return null;
546
+ }
547
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
548
+ }
549
+ const data = await response.json();
550
+ return this.productsDto(data.products);
551
+ } catch (error) {
552
+ this.handleFetchError(
553
+ error,
554
+ "fetching products from collection",
555
+ `${this.baseUrl}collections/${collectionHandle}/products.json`
556
+ );
557
+ }
558
+ }
559
+ /**
560
+ * Validate if a product exists (with caching)
561
+ */
562
+ async validateProductExists(handle) {
563
+ const cacheKey = `product:${handle}`;
564
+ if (this.isCacheValid(cacheKey)) {
565
+ return this.validationCache.get(cacheKey) || false;
566
+ }
567
+ try {
568
+ const url = `${this.baseUrl}products/${handle}.js`;
569
+ const response = await rateLimitedFetch(url, {
570
+ method: "HEAD",
571
+ rateLimitClass: "validate:product"
572
+ });
573
+ const exists = response.ok;
574
+ this.setCacheValue(cacheKey, exists);
575
+ return exists;
576
+ } catch (_error) {
577
+ this.setCacheValue(cacheKey, false);
578
+ return false;
579
+ }
580
+ }
581
+ /**
582
+ * Validate if a collection exists (with caching)
583
+ */
584
+ async validateCollectionExists(handle) {
585
+ const cacheKey = `collection:${handle}`;
586
+ if (this.isCacheValid(cacheKey)) {
587
+ return this.validationCache.get(cacheKey) || false;
588
+ }
589
+ try {
590
+ const url = `${this.baseUrl}collections/${handle}.json`;
591
+ const response = await rateLimitedFetch(url, {
592
+ method: "HEAD",
593
+ rateLimitClass: "validate:collection"
594
+ });
595
+ const exists = response.ok;
596
+ this.setCacheValue(cacheKey, exists);
597
+ return exists;
598
+ } catch (_error) {
599
+ this.setCacheValue(cacheKey, false);
600
+ return false;
601
+ }
602
+ }
603
+ /**
604
+ * Check if cache entry is still valid
605
+ */
606
+ isCacheValid(key) {
607
+ const timestamp = this.cacheTimestamps.get(key);
608
+ if (!timestamp) return false;
609
+ return Date.now() - timestamp < this.cacheExpiry;
610
+ }
611
+ /**
612
+ * Set cache value with timestamp
613
+ */
614
+ setCacheValue(key, value) {
615
+ this.validationCache.set(key, value);
616
+ this.cacheTimestamps.set(key, Date.now());
617
+ }
618
+ /**
619
+ * Validate links in batches to avoid overwhelming the server
620
+ */
621
+ async validateLinksInBatches(items, validator, batchSize = 10) {
622
+ const validItems = [];
623
+ for (let i = 0; i < items.length; i += batchSize) {
624
+ const batch = items.slice(i, i + batchSize);
625
+ const validationPromises = batch.map(async (item) => {
626
+ const isValid = await validator(item);
627
+ return isValid ? item : null;
628
+ });
629
+ const results = await Promise.all(validationPromises);
630
+ const validBatchItems = results.filter(
631
+ (item) => item !== null
632
+ );
633
+ validItems.push(...validBatchItems);
634
+ if (i + batchSize < items.length) {
635
+ await new Promise((resolve) => setTimeout(resolve, 100));
636
+ }
637
+ }
638
+ return validItems;
639
+ }
640
+ /**
641
+ * Fetches comprehensive store information including metadata, social links, and showcase content.
642
+ *
643
+ * @returns {Promise<StoreInfo>} Store information object containing:
644
+ * - `name` - Store name from meta tags or domain
645
+ * - `domain` - Store domain URL
646
+ * - `slug` - Generated store slug
647
+ * - `title` - Store title from meta tags
648
+ * - `description` - Store description from meta tags
649
+ * - `logoUrl` - Store logo URL from Open Graph or CDN
650
+ * - `socialLinks` - Object with social media links (facebook, twitter, instagram, etc.)
651
+ * - `contactLinks` - Object with contact information (tel, email, contactPage)
652
+ * - `headerLinks` - Array of navigation links from header
653
+ * - `showcase` - Object with featured products and collections from homepage
654
+ * - `jsonLdData` - Structured data from JSON-LD scripts
655
+ * - `techProvider` - Shopify-specific information (walletId, subDomain)
656
+ * - `country` - Country detection results with ISO 3166-1 alpha-2 codes (e.g., "US", "GB")
657
+ *
658
+ * @throws {Error} When the store URL is unreachable or returns an error
659
+ *
660
+ * @example
661
+ * ```typescript
662
+ * const shop = new ShopClient('https://exampleshop.com');
663
+ * const storeInfo = await shop.getInfo();
664
+ *
665
+ * console.log(storeInfo.name); // "Example Store"
666
+ * console.log(storeInfo.socialLinks.instagram); // "https://instagram.com/example"
667
+ * console.log(storeInfo.showcase.products); // ["product-handle-1", "product-handle-2"]
668
+ * console.log(storeInfo.country); // "US"
669
+ * ```
670
+ */
671
+ /**
672
+ * Optionally bypass cache and force a fresh fetch.
673
+ *
674
+ * @param options - `{ force?: boolean }` when `true`, ignores cached value and TTL.
675
+ */
676
+ async getInfo(options) {
677
+ try {
678
+ if ((options == null ? void 0 : options.force) === true) {
679
+ this.clearInfoCache();
680
+ }
681
+ if (this.infoCacheValue && this.infoCacheTimestamp !== void 0 && Date.now() - this.infoCacheTimestamp < this.cacheExpiry) {
682
+ return this.infoCacheValue;
683
+ }
684
+ if (this.infoInFlight) {
685
+ return await this.infoInFlight;
686
+ }
687
+ this.infoInFlight = (async () => {
688
+ const { info, currencyCode } = await getInfoForStore({
689
+ baseUrl: this.baseUrl,
690
+ storeDomain: this.storeDomain,
691
+ validateProductExists: (handle) => this.validateProductExists(handle),
692
+ validateCollectionExists: (handle) => this.validateCollectionExists(handle),
693
+ validateLinksInBatches: (items, validator, batchSize) => this.validateLinksInBatches(items, validator, batchSize)
694
+ });
695
+ if (typeof currencyCode === "string") {
696
+ this.storeCurrency = currencyCode;
697
+ }
698
+ this.infoCacheValue = info;
699
+ this.infoCacheTimestamp = Date.now();
700
+ return info;
701
+ })();
702
+ try {
703
+ const result = await this.infoInFlight;
704
+ return result;
705
+ } finally {
706
+ this.infoInFlight = void 0;
707
+ }
708
+ } catch (error) {
709
+ throw this.handleFetchError(error, "fetching store info", this.baseUrl);
710
+ }
711
+ }
712
+ /**
713
+ * Manually clear the cached store info.
714
+ * The next call to `getInfo()` will fetch fresh data regardless of TTL.
715
+ */
716
+ clearInfoCache() {
717
+ this.infoCacheValue = void 0;
718
+ this.infoCacheTimestamp = void 0;
719
+ }
720
+ /**
721
+ * Determine the store's primary vertical and target audience.
722
+ * Uses `getInfo()` internally; no input required.
723
+ */
724
+ async determineStoreType(options) {
725
+ try {
726
+ const breakdown = await determineStoreTypeForStore({
727
+ baseUrl: this.baseUrl,
728
+ getInfo: () => this.getInfo(),
729
+ findProduct: (handle) => this.products.find(handle),
730
+ apiKey: options == null ? void 0 : options.apiKey,
731
+ model: options == null ? void 0 : options.model,
732
+ maxShowcaseProducts: options == null ? void 0 : options.maxShowcaseProducts,
733
+ maxShowcaseCollections: options == null ? void 0 : options.maxShowcaseCollections
734
+ });
735
+ return breakdown;
736
+ } catch (error) {
737
+ throw this.handleFetchError(error, "determineStoreType", this.baseUrl);
738
+ }
739
+ }
740
+ };
741
+ export {
742
+ ShopClient,
743
+ calculateDiscount,
744
+ classifyProduct,
745
+ configureRateLimit,
746
+ detectShopCountry,
747
+ extractDomainWithoutSuffix,
748
+ genProductSlug,
749
+ generateSEOContent,
750
+ generateStoreSlug,
751
+ safeParseDate,
752
+ sanitizeDomain
753
+ };