shop-client 3.8.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.
- package/LICENSE +21 -0
- package/README.md +912 -0
- package/dist/checkout.d.mts +31 -0
- package/dist/checkout.d.ts +31 -0
- package/dist/checkout.js +115 -0
- package/dist/checkout.js.map +1 -0
- package/dist/checkout.mjs +7 -0
- package/dist/checkout.mjs.map +1 -0
- package/dist/chunk-2KBOKOAD.mjs +177 -0
- package/dist/chunk-2KBOKOAD.mjs.map +1 -0
- package/dist/chunk-BWKBRM2Z.mjs +136 -0
- package/dist/chunk-BWKBRM2Z.mjs.map +1 -0
- package/dist/chunk-O4BPIIQ6.mjs +503 -0
- package/dist/chunk-O4BPIIQ6.mjs.map +1 -0
- package/dist/chunk-QCTICSBE.mjs +398 -0
- package/dist/chunk-QCTICSBE.mjs.map +1 -0
- package/dist/chunk-QL5OUZGP.mjs +91 -0
- package/dist/chunk-QL5OUZGP.mjs.map +1 -0
- package/dist/chunk-WTK5HUFI.mjs +1287 -0
- package/dist/chunk-WTK5HUFI.mjs.map +1 -0
- package/dist/collections.d.mts +64 -0
- package/dist/collections.d.ts +64 -0
- package/dist/collections.js +540 -0
- package/dist/collections.js.map +1 -0
- package/dist/collections.mjs +9 -0
- package/dist/collections.mjs.map +1 -0
- package/dist/index.d.mts +233 -0
- package/dist/index.d.ts +233 -0
- package/dist/index.js +3241 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +702 -0
- package/dist/index.mjs.map +1 -0
- package/dist/products.d.mts +63 -0
- package/dist/products.d.ts +63 -0
- package/dist/products.js +1206 -0
- package/dist/products.js.map +1 -0
- package/dist/products.mjs +9 -0
- package/dist/products.mjs.map +1 -0
- package/dist/store-CJVUz2Yb.d.mts +608 -0
- package/dist/store-CJVUz2Yb.d.ts +608 -0
- package/dist/store.d.mts +1 -0
- package/dist/store.d.ts +1 -0
- package/dist/store.js +698 -0
- package/dist/store.js.map +1 -0
- package/dist/store.mjs +9 -0
- package/dist/store.mjs.map +1 -0
- package/dist/utils/rate-limit.d.mts +25 -0
- package/dist/utils/rate-limit.d.ts +25 -0
- package/dist/utils/rate-limit.js +203 -0
- package/dist/utils/rate-limit.js.map +1 -0
- package/dist/utils/rate-limit.mjs +11 -0
- package/dist/utils/rate-limit.mjs.map +1 -0
- package/package.json +116 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
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
|