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