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.
- package/README.md +65 -0
- package/dist/ai/enrich.d.ts +93 -0
- package/dist/ai/enrich.js +25 -0
- package/dist/checkout.js +6 -0
- package/dist/chunk-2MF53V33.js +196 -0
- package/dist/chunk-CN7L3BHG.js +147 -0
- package/dist/chunk-CXUCPK6X.js +460 -0
- package/dist/chunk-DJQEZNHG.js +233 -0
- package/dist/chunk-MOBWPEY4.js +420 -0
- package/dist/chunk-QUDGES3A.js +195 -0
- package/dist/chunk-RR6YTQWP.js +90 -0
- package/dist/chunk-VPPCOJC3.js +865 -0
- package/dist/collections.d.ts +2 -1
- package/dist/collections.js +8 -0
- package/dist/index.d.ts +7 -84
- package/dist/index.js +753 -0
- package/dist/products.d.ts +2 -1
- package/dist/products.js +8 -0
- package/dist/store.d.ts +53 -1
- package/dist/store.js +9 -0
- package/dist/{store-iQARl6J3.d.ts → types-luPg5O08.d.ts} +1 -208
- package/dist/utils/detect-country.d.ts +32 -0
- package/dist/utils/detect-country.js +6 -0
- package/dist/utils/func.d.ts +61 -0
- package/dist/utils/func.js +24 -0
- package/dist/utils/rate-limit.js +10 -0
- package/package.json +16 -3
- package/dist/checkout.mjs +0 -1
- package/dist/chunk-6GPWNCDO.mjs +0 -130
- package/dist/chunk-EJO5U4BT.mjs +0 -2
- package/dist/chunk-FFKWCNLU.mjs +0 -1
- package/dist/chunk-KYLPIEU3.mjs +0 -2
- package/dist/chunk-MB2INNNP.mjs +0 -1
- package/dist/chunk-MI7754VX.mjs +0 -2
- package/dist/chunk-SZQPMLZG.mjs +0 -1
- package/dist/collections.mjs +0 -1
- package/dist/enrich-OZHBXKK6.mjs +0 -1
- package/dist/index.mjs +0 -2
- package/dist/products.mjs +0 -1
- package/dist/store.mjs +0 -1
- package/dist/utils/rate-limit.mjs +0 -1
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import {
|
|
2
|
+
rateLimitedFetch
|
|
3
|
+
} from "./chunk-2MF53V33.js";
|
|
4
|
+
import {
|
|
5
|
+
formatPrice
|
|
6
|
+
} from "./chunk-CN7L3BHG.js";
|
|
7
|
+
|
|
8
|
+
// src/products.ts
|
|
9
|
+
import { filter, isNonNullish } from "remeda";
|
|
10
|
+
function createProductOperations(baseUrl, storeDomain, fetchProducts, productsDto, productDto, getStoreInfo, findProduct) {
|
|
11
|
+
const cacheExpiryMs = 5 * 60 * 1e3;
|
|
12
|
+
const findCache = /* @__PURE__ */ new Map();
|
|
13
|
+
const getCached = (key) => {
|
|
14
|
+
const entry = findCache.get(key);
|
|
15
|
+
if (!entry) return void 0;
|
|
16
|
+
if (Date.now() - entry.ts < cacheExpiryMs) return entry.value;
|
|
17
|
+
findCache.delete(key);
|
|
18
|
+
return void 0;
|
|
19
|
+
};
|
|
20
|
+
const setCached = (key, value) => {
|
|
21
|
+
findCache.set(key, { ts: Date.now(), value });
|
|
22
|
+
};
|
|
23
|
+
function applyCurrencyOverride(product, currency) {
|
|
24
|
+
var _a, _b, _c, _d, _e, _f;
|
|
25
|
+
const priceMin = (_b = (_a = product.priceMin) != null ? _a : product.price) != null ? _b : 0;
|
|
26
|
+
const priceMax = (_d = (_c = product.priceMax) != null ? _c : product.price) != null ? _d : 0;
|
|
27
|
+
const compareAtMin = (_f = (_e = product.compareAtPriceMin) != null ? _e : product.compareAtPrice) != null ? _f : 0;
|
|
28
|
+
return {
|
|
29
|
+
...product,
|
|
30
|
+
currency,
|
|
31
|
+
localizedPricing: {
|
|
32
|
+
currency,
|
|
33
|
+
priceFormatted: formatPrice(priceMin, currency),
|
|
34
|
+
priceMinFormatted: formatPrice(priceMin, currency),
|
|
35
|
+
priceMaxFormatted: formatPrice(priceMax, currency),
|
|
36
|
+
compareAtPriceFormatted: formatPrice(compareAtMin, currency)
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function maybeOverrideProductsCurrency(products, currency) {
|
|
41
|
+
if (!products || !currency) return products;
|
|
42
|
+
return products.map((p) => applyCurrencyOverride(p, currency));
|
|
43
|
+
}
|
|
44
|
+
const operations = {
|
|
45
|
+
/**
|
|
46
|
+
* Fetches all products from the store across all pages.
|
|
47
|
+
*
|
|
48
|
+
* @returns {Promise<Product[] | null>} Array of all products or null if error occurs
|
|
49
|
+
*
|
|
50
|
+
* @throws {Error} When there's a network error or API failure
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const shop = new ShopClient('https://exampleshop.com');
|
|
55
|
+
* const allProducts = await shop.products.all();
|
|
56
|
+
*
|
|
57
|
+
* console.log(`Found ${allProducts?.length} products`);
|
|
58
|
+
* allProducts?.forEach(product => {
|
|
59
|
+
* console.log(product.title, product.price);
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
all: async (options) => {
|
|
64
|
+
const limit = 250;
|
|
65
|
+
const allProducts = [];
|
|
66
|
+
async function fetchAll() {
|
|
67
|
+
let currentPage = 1;
|
|
68
|
+
while (true) {
|
|
69
|
+
const products = await fetchProducts(currentPage, limit);
|
|
70
|
+
if (!products || products.length === 0 || products.length < limit) {
|
|
71
|
+
if (products && products.length > 0) {
|
|
72
|
+
allProducts.push(...products);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
allProducts.push(...products);
|
|
77
|
+
currentPage++;
|
|
78
|
+
}
|
|
79
|
+
return allProducts;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const products = await fetchAll();
|
|
83
|
+
return maybeOverrideProductsCurrency(products, options == null ? void 0 : options.currency);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error("Failed to fetch all products:", storeDomain, error);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
/**
|
|
90
|
+
* Fetches products with pagination support.
|
|
91
|
+
*
|
|
92
|
+
* @param options - Pagination options
|
|
93
|
+
* @param options.page - Page number (default: 1)
|
|
94
|
+
* @param options.limit - Number of products per page (default: 250, max: 250)
|
|
95
|
+
*
|
|
96
|
+
* @returns {Promise<Product[] | null>} Array of products for the specified page or null if error occurs
|
|
97
|
+
*
|
|
98
|
+
* @throws {Error} When there's a network error or API failure
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const shop = new ShopClient('https://example.myshopify.com');
|
|
103
|
+
*
|
|
104
|
+
* // Get first page with default limit (250)
|
|
105
|
+
* const firstPage = await shop.products.paginated();
|
|
106
|
+
*
|
|
107
|
+
* // Get second page with custom limit
|
|
108
|
+
* const secondPage = await shop.products.paginated({ page: 2, limit: 50 });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
paginated: async (options) => {
|
|
112
|
+
var _a, _b;
|
|
113
|
+
const page = (_a = options == null ? void 0 : options.page) != null ? _a : 1;
|
|
114
|
+
const limit = Math.min((_b = options == null ? void 0 : options.limit) != null ? _b : 250, 250);
|
|
115
|
+
const url = `${baseUrl}products.json?limit=${limit}&page=${page}`;
|
|
116
|
+
try {
|
|
117
|
+
const response = await rateLimitedFetch(url, {
|
|
118
|
+
rateLimitClass: "products:paginated"
|
|
119
|
+
});
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
console.error(
|
|
122
|
+
`HTTP error! status: ${response.status} for ${storeDomain} page ${page}`
|
|
123
|
+
);
|
|
124
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
125
|
+
}
|
|
126
|
+
const data = await response.json();
|
|
127
|
+
if (data.products.length === 0) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
const normalized = productsDto(data.products);
|
|
131
|
+
return maybeOverrideProductsCurrency(normalized, options == null ? void 0 : options.currency);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(
|
|
134
|
+
`Error fetching products for ${storeDomain} page ${page} with limit ${limit}:`,
|
|
135
|
+
error
|
|
136
|
+
);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
/**
|
|
141
|
+
* Finds a specific product by its handle.
|
|
142
|
+
*
|
|
143
|
+
* @param productHandle - The product handle (URL slug) to search for
|
|
144
|
+
*
|
|
145
|
+
* @returns {Promise<Product | null>} The product if found, null if not found
|
|
146
|
+
*
|
|
147
|
+
* @throws {Error} When the handle is invalid or there's a network error
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* const shop = new ShopClient('https://exampleshop.com');
|
|
152
|
+
*
|
|
153
|
+
* // Find product by handle
|
|
154
|
+
* const product = await shop.products.find('awesome-t-shirt');
|
|
155
|
+
*
|
|
156
|
+
* if (product) {
|
|
157
|
+
* console.log(product.title, product.price);
|
|
158
|
+
* console.log('Available variants:', product.variants.length);
|
|
159
|
+
* }
|
|
160
|
+
*
|
|
161
|
+
* // Handle with query string
|
|
162
|
+
* const productWithVariant = await shop.products.find('t-shirt?variant=123');
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
find: async (productHandle, options) => {
|
|
166
|
+
var _a, _b;
|
|
167
|
+
if (!productHandle || typeof productHandle !== "string") {
|
|
168
|
+
throw new Error("Product handle is required and must be a string");
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
let qs = null;
|
|
172
|
+
if (productHandle.includes("?")) {
|
|
173
|
+
const parts = productHandle.split("?");
|
|
174
|
+
const handlePart = (_a = parts[0]) != null ? _a : productHandle;
|
|
175
|
+
const qsPart = (_b = parts[1]) != null ? _b : null;
|
|
176
|
+
productHandle = handlePart;
|
|
177
|
+
qs = qsPart;
|
|
178
|
+
}
|
|
179
|
+
const sanitizedHandle = productHandle.trim().replace(/[^a-zA-Z0-9\-_]/g, "");
|
|
180
|
+
if (!sanitizedHandle) {
|
|
181
|
+
throw new Error("Invalid product handle format");
|
|
182
|
+
}
|
|
183
|
+
if (sanitizedHandle.length > 255) {
|
|
184
|
+
throw new Error("Product handle is too long");
|
|
185
|
+
}
|
|
186
|
+
const cached = getCached(sanitizedHandle);
|
|
187
|
+
if (typeof cached !== "undefined") {
|
|
188
|
+
return (options == null ? void 0 : options.currency) ? cached ? applyCurrencyOverride(cached, options.currency) : null : cached;
|
|
189
|
+
}
|
|
190
|
+
let finalHandle = sanitizedHandle;
|
|
191
|
+
try {
|
|
192
|
+
const htmlResp = await rateLimitedFetch(
|
|
193
|
+
`${baseUrl}products/${encodeURIComponent(sanitizedHandle)}`,
|
|
194
|
+
{ rateLimitClass: "products:resolve" }
|
|
195
|
+
);
|
|
196
|
+
if (htmlResp.ok) {
|
|
197
|
+
const finalUrl = htmlResp.url;
|
|
198
|
+
if (finalUrl) {
|
|
199
|
+
const pathname = new URL(finalUrl).pathname.replace(/\/$/, "");
|
|
200
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
201
|
+
const idx = parts.indexOf("products");
|
|
202
|
+
const maybeHandle = idx >= 0 ? parts[idx + 1] : void 0;
|
|
203
|
+
if (typeof maybeHandle === "string" && maybeHandle.length) {
|
|
204
|
+
finalHandle = maybeHandle;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
const url = `${baseUrl}products/${encodeURIComponent(finalHandle)}.js${qs ? `?${qs}` : ""}`;
|
|
211
|
+
const response = await rateLimitedFetch(url, {
|
|
212
|
+
rateLimitClass: "products:single"
|
|
213
|
+
});
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
if (response.status === 404) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
219
|
+
}
|
|
220
|
+
const product = await response.json();
|
|
221
|
+
const productData = productDto(product);
|
|
222
|
+
setCached(sanitizedHandle, productData);
|
|
223
|
+
if (finalHandle !== sanitizedHandle)
|
|
224
|
+
setCached(finalHandle, productData);
|
|
225
|
+
return (options == null ? void 0 : options.currency) ? applyCurrencyOverride(productData, options.currency) : productData;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error instanceof Error) {
|
|
228
|
+
console.error(
|
|
229
|
+
`Error fetching product ${productHandle}:`,
|
|
230
|
+
baseUrl,
|
|
231
|
+
error.message
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
/**
|
|
238
|
+
* Enrich a product by generating merged markdown from body_html and product page.
|
|
239
|
+
* Adds `enriched_content` to the returned product.
|
|
240
|
+
*/
|
|
241
|
+
enriched: async (productHandle, options) => {
|
|
242
|
+
if (!productHandle || typeof productHandle !== "string") {
|
|
243
|
+
throw new Error("Product handle is required and must be a string");
|
|
244
|
+
}
|
|
245
|
+
const apiKey = (options == null ? void 0 : options.apiKey) || process.env.OPENROUTER_API_KEY;
|
|
246
|
+
if (!apiKey) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
"Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY."
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const baseProduct = await operations.find(productHandle);
|
|
252
|
+
if (!baseProduct) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const handle = baseProduct.handle;
|
|
256
|
+
const { enrichProduct } = await import("./ai/enrich.js");
|
|
257
|
+
const enriched = await enrichProduct(storeDomain, handle, {
|
|
258
|
+
apiKey,
|
|
259
|
+
useGfm: options == null ? void 0 : options.useGfm,
|
|
260
|
+
inputType: options == null ? void 0 : options.inputType,
|
|
261
|
+
model: options == null ? void 0 : options.model,
|
|
262
|
+
outputFormat: options == null ? void 0 : options.outputFormat
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
...baseProduct,
|
|
266
|
+
enriched_content: enriched.mergedMarkdown
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
classify: async (productHandle, options) => {
|
|
270
|
+
if (!productHandle || typeof productHandle !== "string") {
|
|
271
|
+
throw new Error("Product handle is required and must be a string");
|
|
272
|
+
}
|
|
273
|
+
const apiKey = (options == null ? void 0 : options.apiKey) || process.env.OPENROUTER_API_KEY;
|
|
274
|
+
if (!apiKey) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
"Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY."
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const enrichedProduct = await operations.enriched(productHandle, {
|
|
280
|
+
apiKey,
|
|
281
|
+
inputType: "html",
|
|
282
|
+
model: options == null ? void 0 : options.model,
|
|
283
|
+
outputFormat: "json"
|
|
284
|
+
});
|
|
285
|
+
if (!enrichedProduct || !enrichedProduct.enriched_content) return null;
|
|
286
|
+
let productContent = enrichedProduct.enriched_content;
|
|
287
|
+
try {
|
|
288
|
+
const obj = JSON.parse(enrichedProduct.enriched_content);
|
|
289
|
+
const lines = [];
|
|
290
|
+
if (obj.title && typeof obj.title === "string")
|
|
291
|
+
lines.push(`Title: ${obj.title}`);
|
|
292
|
+
if (obj.description && typeof obj.description === "string")
|
|
293
|
+
lines.push(`Description: ${obj.description}`);
|
|
294
|
+
if (Array.isArray(obj.materials) && obj.materials.length)
|
|
295
|
+
lines.push(`Materials: ${obj.materials.join(", ")}`);
|
|
296
|
+
if (Array.isArray(obj.care) && obj.care.length)
|
|
297
|
+
lines.push(`Care: ${obj.care.join(", ")}`);
|
|
298
|
+
if (obj.fit && typeof obj.fit === "string")
|
|
299
|
+
lines.push(`Fit: ${obj.fit}`);
|
|
300
|
+
if (obj.returnPolicy && typeof obj.returnPolicy === "string")
|
|
301
|
+
lines.push(`ReturnPolicy: ${obj.returnPolicy}`);
|
|
302
|
+
productContent = lines.join("\n");
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
const { classifyProduct } = await import("./ai/enrich.js");
|
|
306
|
+
const classification = await classifyProduct(productContent, {
|
|
307
|
+
apiKey,
|
|
308
|
+
model: options == null ? void 0 : options.model
|
|
309
|
+
});
|
|
310
|
+
return classification;
|
|
311
|
+
},
|
|
312
|
+
generateSEOContent: async (productHandle, options) => {
|
|
313
|
+
if (!productHandle || typeof productHandle !== "string") {
|
|
314
|
+
throw new Error("Product handle is required and must be a string");
|
|
315
|
+
}
|
|
316
|
+
const apiKey = (options == null ? void 0 : options.apiKey) || process.env.OPENROUTER_API_KEY;
|
|
317
|
+
if (!apiKey) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
"Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY."
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const baseProduct = await operations.find(productHandle);
|
|
323
|
+
if (!baseProduct) return null;
|
|
324
|
+
const payload = {
|
|
325
|
+
title: baseProduct.title,
|
|
326
|
+
description: baseProduct.bodyHtml || void 0,
|
|
327
|
+
vendor: baseProduct.vendor,
|
|
328
|
+
price: baseProduct.price,
|
|
329
|
+
tags: baseProduct.tags
|
|
330
|
+
};
|
|
331
|
+
const { generateSEOContent: generateSEOContentLLM } = await import("./ai/enrich.js");
|
|
332
|
+
const seo = await generateSEOContentLLM(payload, {
|
|
333
|
+
apiKey,
|
|
334
|
+
model: options == null ? void 0 : options.model
|
|
335
|
+
});
|
|
336
|
+
return seo;
|
|
337
|
+
},
|
|
338
|
+
/**
|
|
339
|
+
* Fetches products that are showcased/featured on the store's homepage.
|
|
340
|
+
*
|
|
341
|
+
* @returns {Promise<Product[]>} Array of showcased products found on the homepage
|
|
342
|
+
*
|
|
343
|
+
* @throws {Error} When there's a network error or API failure
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* ```typescript
|
|
347
|
+
* const shop = new ShopClient('https://exampleshop.com');
|
|
348
|
+
* const showcasedProducts = await shop.products.showcased();
|
|
349
|
+
*
|
|
350
|
+
* console.log(`Found ${showcasedProducts.length} showcased products`);
|
|
351
|
+
* showcasedProducts.forEach(product => {
|
|
352
|
+
* console.log(`Featured: ${product.title} - ${product.price}`);
|
|
353
|
+
* });
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
showcased: async () => {
|
|
357
|
+
const storeInfo = await getStoreInfo();
|
|
358
|
+
const products = await Promise.all(
|
|
359
|
+
storeInfo.showcase.products.map(
|
|
360
|
+
(productHandle) => findProduct(productHandle)
|
|
361
|
+
)
|
|
362
|
+
);
|
|
363
|
+
return filter(products, isNonNullish);
|
|
364
|
+
},
|
|
365
|
+
/**
|
|
366
|
+
* Creates a filter map of variant options and their distinct values from all products.
|
|
367
|
+
*
|
|
368
|
+
* @returns {Promise<Record<string, string[]> | null>} Map of option names to their distinct values or null if error occurs
|
|
369
|
+
*
|
|
370
|
+
* @throws {Error} When there's a network error or API failure
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```typescript
|
|
374
|
+
* const shop = new ShopClient('https://exampleshop.com');
|
|
375
|
+
* const filters = await shop.products.filter();
|
|
376
|
+
*
|
|
377
|
+
* console.log('Available filters:', filters);
|
|
378
|
+
* // Output: { "Size": ["S", "M", "L", "XL"], "Color": ["Red", "Blue", "Green"] }
|
|
379
|
+
*
|
|
380
|
+
* // Use filters for UI components
|
|
381
|
+
* Object.entries(filters || {}).forEach(([optionName, values]) => {
|
|
382
|
+
* console.log(`${optionName}: ${values.join(', ')}`);
|
|
383
|
+
* });
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
filter: async () => {
|
|
387
|
+
try {
|
|
388
|
+
const products = await operations.all();
|
|
389
|
+
if (!products || products.length === 0) {
|
|
390
|
+
return {};
|
|
391
|
+
}
|
|
392
|
+
const filterMap = {};
|
|
393
|
+
products.forEach((product) => {
|
|
394
|
+
if (product.variants && product.variants.length > 0) {
|
|
395
|
+
if (product.options && product.options.length > 0) {
|
|
396
|
+
product.options.forEach((option) => {
|
|
397
|
+
const lowercaseOptionName = option.name.toLowerCase();
|
|
398
|
+
if (!filterMap[lowercaseOptionName]) {
|
|
399
|
+
filterMap[lowercaseOptionName] = /* @__PURE__ */ new Set();
|
|
400
|
+
}
|
|
401
|
+
option.values.forEach((value) => {
|
|
402
|
+
const trimmed = value == null ? void 0 : value.trim();
|
|
403
|
+
if (trimmed) {
|
|
404
|
+
let set = filterMap[lowercaseOptionName];
|
|
405
|
+
if (!set) {
|
|
406
|
+
set = /* @__PURE__ */ new Set();
|
|
407
|
+
filterMap[lowercaseOptionName] = set;
|
|
408
|
+
}
|
|
409
|
+
set.add(trimmed.toLowerCase());
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
product.variants.forEach((variant) => {
|
|
415
|
+
var _a, _b, _c, _d, _e, _f;
|
|
416
|
+
if (variant.option1) {
|
|
417
|
+
const optionName = (((_b = (_a = product.options) == null ? void 0 : _a[0]) == null ? void 0 : _b.name) || "Option 1").toLowerCase();
|
|
418
|
+
let set1 = filterMap[optionName];
|
|
419
|
+
if (!set1) {
|
|
420
|
+
set1 = /* @__PURE__ */ new Set();
|
|
421
|
+
filterMap[optionName] = set1;
|
|
422
|
+
}
|
|
423
|
+
set1.add(variant.option1.trim().toLowerCase());
|
|
424
|
+
}
|
|
425
|
+
if (variant.option2) {
|
|
426
|
+
const optionName = (((_d = (_c = product.options) == null ? void 0 : _c[1]) == null ? void 0 : _d.name) || "Option 2").toLowerCase();
|
|
427
|
+
let set2 = filterMap[optionName];
|
|
428
|
+
if (!set2) {
|
|
429
|
+
set2 = /* @__PURE__ */ new Set();
|
|
430
|
+
filterMap[optionName] = set2;
|
|
431
|
+
}
|
|
432
|
+
set2.add(variant.option2.trim().toLowerCase());
|
|
433
|
+
}
|
|
434
|
+
if (variant.option3) {
|
|
435
|
+
const optionName = (((_f = (_e = product.options) == null ? void 0 : _e[2]) == null ? void 0 : _f.name) || "Option 3").toLowerCase();
|
|
436
|
+
if (!filterMap[optionName]) {
|
|
437
|
+
filterMap[optionName] = /* @__PURE__ */ new Set();
|
|
438
|
+
}
|
|
439
|
+
filterMap[optionName].add(variant.option3.trim().toLowerCase());
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
const result = {};
|
|
445
|
+
Object.entries(filterMap).forEach(([optionName, valueSet]) => {
|
|
446
|
+
result[optionName] = Array.from(valueSet).sort();
|
|
447
|
+
});
|
|
448
|
+
return result;
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.error("Failed to create product filters:", storeDomain, error);
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
return operations;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export {
|
|
459
|
+
createProductOperations
|
|
460
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import {
|
|
2
|
+
rateLimitedFetch
|
|
3
|
+
} from "./chunk-2MF53V33.js";
|
|
4
|
+
import {
|
|
5
|
+
detectShopCountry
|
|
6
|
+
} from "./chunk-QUDGES3A.js";
|
|
7
|
+
import {
|
|
8
|
+
extractDomainWithoutSuffix,
|
|
9
|
+
generateStoreSlug,
|
|
10
|
+
sanitizeDomain
|
|
11
|
+
} from "./chunk-CN7L3BHG.js";
|
|
12
|
+
|
|
13
|
+
// src/client/get-info.ts
|
|
14
|
+
import { unique } from "remeda";
|
|
15
|
+
async function getInfoForStore(args) {
|
|
16
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
|
|
17
|
+
const {
|
|
18
|
+
baseUrl,
|
|
19
|
+
storeDomain,
|
|
20
|
+
validateProductExists,
|
|
21
|
+
validateCollectionExists,
|
|
22
|
+
validateLinksInBatches
|
|
23
|
+
} = args;
|
|
24
|
+
const response = await rateLimitedFetch(baseUrl, {
|
|
25
|
+
rateLimitClass: "store:info"
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
29
|
+
}
|
|
30
|
+
const html = await response.text();
|
|
31
|
+
const getMetaTag = (name2) => {
|
|
32
|
+
const regex = new RegExp(
|
|
33
|
+
`<meta[^>]*name=["']${name2}["'][^>]*content=["'](.*?)["']`
|
|
34
|
+
);
|
|
35
|
+
const match = html.match(regex);
|
|
36
|
+
return match ? match[1] : null;
|
|
37
|
+
};
|
|
38
|
+
const getPropertyMetaTag = (property) => {
|
|
39
|
+
const regex = new RegExp(
|
|
40
|
+
`<meta[^>]*property=["']${property}["'][^>]*content=["'](.*?)["']`
|
|
41
|
+
);
|
|
42
|
+
const match = html.match(regex);
|
|
43
|
+
return match ? match[1] : null;
|
|
44
|
+
};
|
|
45
|
+
const name = (_a = getMetaTag("og:site_name")) != null ? _a : extractDomainWithoutSuffix(baseUrl);
|
|
46
|
+
const title = (_b = getMetaTag("og:title")) != null ? _b : getMetaTag("twitter:title");
|
|
47
|
+
const description = getMetaTag("description") || getPropertyMetaTag("og:description");
|
|
48
|
+
const shopifyWalletId = (_c = getMetaTag("shopify-digital-wallet")) == null ? void 0 : _c.split("/")[1];
|
|
49
|
+
const myShopifySubdomainMatch = html.match(/['"](.*?\.myshopify\.com)['"]/);
|
|
50
|
+
const myShopifySubdomain = myShopifySubdomainMatch ? myShopifySubdomainMatch[1] : null;
|
|
51
|
+
let logoUrl = getPropertyMetaTag("og:image") || getPropertyMetaTag("og:image:secure_url");
|
|
52
|
+
if (!logoUrl) {
|
|
53
|
+
const logoMatch = html.match(
|
|
54
|
+
/<img[^>]+src=["']([^"']+\/cdn\/shop\/[^"']+)["']/
|
|
55
|
+
);
|
|
56
|
+
const matchedUrl = logoMatch == null ? void 0 : logoMatch[1];
|
|
57
|
+
logoUrl = matchedUrl ? matchedUrl.replace("http://", "https://") : null;
|
|
58
|
+
} else {
|
|
59
|
+
logoUrl = logoUrl.replace("http://", "https://");
|
|
60
|
+
}
|
|
61
|
+
const socialLinks = {};
|
|
62
|
+
const socialRegex = /<a[^>]+href=["']([^"']*(?:facebook|twitter|instagram|pinterest|youtube|linkedin|tiktok|vimeo)\.com[^"']*)["']/g;
|
|
63
|
+
for (const match of html.matchAll(socialRegex)) {
|
|
64
|
+
const str = match[1];
|
|
65
|
+
if (!str) continue;
|
|
66
|
+
let href = str;
|
|
67
|
+
try {
|
|
68
|
+
if (href.startsWith("//")) {
|
|
69
|
+
href = `https:${href}`;
|
|
70
|
+
} else if (href.startsWith("/")) {
|
|
71
|
+
href = new URL(href, baseUrl).toString();
|
|
72
|
+
}
|
|
73
|
+
const parsed = new URL(href);
|
|
74
|
+
const domain = parsed.hostname.replace("www.", "").split(".")[0];
|
|
75
|
+
if (domain) {
|
|
76
|
+
socialLinks[domain] = parsed.toString();
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const contactLinks = {
|
|
82
|
+
tel: null,
|
|
83
|
+
email: null,
|
|
84
|
+
contactPage: null
|
|
85
|
+
};
|
|
86
|
+
for (const match of html.matchAll(/href=["']tel:([^"']+)["']/g)) {
|
|
87
|
+
contactLinks.tel = ((_d = match == null ? void 0 : match[1]) == null ? void 0 : _d.trim()) || null;
|
|
88
|
+
}
|
|
89
|
+
for (const match of html.matchAll(/href=["']mailto:([^"']+)["']/g)) {
|
|
90
|
+
contactLinks.email = ((_e = match == null ? void 0 : match[1]) == null ? void 0 : _e.trim()) || null;
|
|
91
|
+
}
|
|
92
|
+
for (const match of html.matchAll(
|
|
93
|
+
/href=["']([^"']*(?:\/contact|\/pages\/contact)[^"']*)["']/g
|
|
94
|
+
)) {
|
|
95
|
+
contactLinks.contactPage = (match == null ? void 0 : match[1]) || null;
|
|
96
|
+
}
|
|
97
|
+
const extractedProductLinks = ((_g = (_f = html.match(/href=["']([^"']*\/products\/[^"']+)["']/g)) == null ? void 0 : _f.map(
|
|
98
|
+
(match) => {
|
|
99
|
+
var _a2, _b2;
|
|
100
|
+
return (_b2 = (_a2 = match == null ? void 0 : match.split("href=")[1]) == null ? void 0 : _a2.replace(/["']/g, "")) == null ? void 0 : _b2.split("/").at(-1);
|
|
101
|
+
}
|
|
102
|
+
)) == null ? void 0 : _g.filter(Boolean)) || [];
|
|
103
|
+
const extractedCollectionLinks = ((_i = (_h = html.match(/href=["']([^"']*\/collections\/[^"']+)["']/g)) == null ? void 0 : _h.map(
|
|
104
|
+
(match) => {
|
|
105
|
+
var _a2, _b2;
|
|
106
|
+
return (_b2 = (_a2 = match == null ? void 0 : match.split("href=")[1]) == null ? void 0 : _a2.replace(/["']/g, "")) == null ? void 0 : _b2.split("/").at(-1);
|
|
107
|
+
}
|
|
108
|
+
)) == null ? void 0 : _i.filter(Boolean)) || [];
|
|
109
|
+
const headerLinks = (_k = (_j = html.match(
|
|
110
|
+
/<(header|nav|div|section)\b[^>]*\b(?:id|class)=["'][^"']*(?=.*shopify-section)(?=.*\b(header|navigation|nav|menu)\b)[^"']*["'][^>]*>[\s\S]*?<\/\1>/gi
|
|
111
|
+
)) == null ? void 0 : _j.flatMap((header) => {
|
|
112
|
+
var _a2, _b2;
|
|
113
|
+
const links = (_a2 = header.match(/href=["']([^"']+)["']/g)) == null ? void 0 : _a2.filter(
|
|
114
|
+
(link) => link.includes("/products/") || link.includes("/collections/") || link.includes("/pages/")
|
|
115
|
+
);
|
|
116
|
+
return (_b2 = links == null ? void 0 : links.map((link) => {
|
|
117
|
+
var _a3;
|
|
118
|
+
const href = (_a3 = link.match(/href=["']([^"']+)["']/)) == null ? void 0 : _a3[1];
|
|
119
|
+
if (href && !href.startsWith("#") && !href.startsWith("javascript:")) {
|
|
120
|
+
try {
|
|
121
|
+
const url = new URL(href, storeDomain);
|
|
122
|
+
return url.pathname.replace(/^\/|\/$/g, "");
|
|
123
|
+
} catch {
|
|
124
|
+
return href.replace(/^\/|\/$/g, "");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}).filter((item) => Boolean(item))) != null ? _b2 : [];
|
|
129
|
+
})) != null ? _k : [];
|
|
130
|
+
const slug = generateStoreSlug(baseUrl);
|
|
131
|
+
const countryDetection = await detectShopCountry(html);
|
|
132
|
+
const [homePageProductLinks, homePageCollectionLinks] = await Promise.all([
|
|
133
|
+
validateLinksInBatches(
|
|
134
|
+
extractedProductLinks.filter(
|
|
135
|
+
(handle) => Boolean(handle)
|
|
136
|
+
),
|
|
137
|
+
(handle) => validateProductExists(handle)
|
|
138
|
+
),
|
|
139
|
+
validateLinksInBatches(
|
|
140
|
+
extractedCollectionLinks.filter(
|
|
141
|
+
(handle) => Boolean(handle)
|
|
142
|
+
),
|
|
143
|
+
(handle) => validateCollectionExists(handle)
|
|
144
|
+
)
|
|
145
|
+
]);
|
|
146
|
+
const info = {
|
|
147
|
+
name: name || slug,
|
|
148
|
+
domain: sanitizeDomain(baseUrl),
|
|
149
|
+
slug,
|
|
150
|
+
title: title || null,
|
|
151
|
+
description: description || null,
|
|
152
|
+
logoUrl,
|
|
153
|
+
socialLinks,
|
|
154
|
+
contactLinks,
|
|
155
|
+
headerLinks,
|
|
156
|
+
showcase: {
|
|
157
|
+
products: unique(homePageProductLinks != null ? homePageProductLinks : []),
|
|
158
|
+
collections: unique(homePageCollectionLinks != null ? homePageCollectionLinks : [])
|
|
159
|
+
},
|
|
160
|
+
jsonLdData: ((_m = (_l = html.match(
|
|
161
|
+
/<script[^>]*type="application\/ld\+json"[^>]*>([^<]+)<\/script>/g
|
|
162
|
+
)) == null ? void 0 : _l.map(
|
|
163
|
+
(match) => {
|
|
164
|
+
var _a2;
|
|
165
|
+
return ((_a2 = match == null ? void 0 : match.split(">")[1]) == null ? void 0 : _a2.replace(/<\/script/g, "")) || null;
|
|
166
|
+
}
|
|
167
|
+
)) == null ? void 0 : _m.map((json) => json ? JSON.parse(json) : null)) || [],
|
|
168
|
+
techProvider: {
|
|
169
|
+
name: "shopify",
|
|
170
|
+
walletId: shopifyWalletId,
|
|
171
|
+
subDomain: myShopifySubdomain != null ? myShopifySubdomain : null
|
|
172
|
+
},
|
|
173
|
+
country: countryDetection.country
|
|
174
|
+
};
|
|
175
|
+
const currencyCode = countryDetection == null ? void 0 : countryDetection.currencyCode;
|
|
176
|
+
return { info, currencyCode };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/store.ts
|
|
180
|
+
function createStoreOperations(context) {
|
|
181
|
+
return {
|
|
182
|
+
/**
|
|
183
|
+
* Fetches comprehensive store information including metadata, social links, and showcase content.
|
|
184
|
+
*
|
|
185
|
+
* @returns {Promise<StoreInfo>} Store information object containing:
|
|
186
|
+
* - `name` - Store name from meta tags or domain
|
|
187
|
+
* - `domain` - Store domain URL
|
|
188
|
+
* - `slug` - Generated store slug
|
|
189
|
+
* - `title` - Store title from meta tags
|
|
190
|
+
* - `description` - Store description from meta tags
|
|
191
|
+
* - `logoUrl` - Store logo URL from Open Graph or CDN
|
|
192
|
+
* - `socialLinks` - Object with social media links (facebook, twitter, instagram, etc.)
|
|
193
|
+
* - `contactLinks` - Object with contact information (tel, email, contactPage)
|
|
194
|
+
* - `headerLinks` - Array of navigation links from header
|
|
195
|
+
* - `showcase` - Object with featured products and collections from homepage
|
|
196
|
+
* - `jsonLdData` - Structured data from JSON-LD scripts
|
|
197
|
+
* - `techProvider` - Shopify-specific information (walletId, subDomain)
|
|
198
|
+
* - `country` - Country detection results with ISO 3166-1 alpha-2 codes (e.g., "US", "GB")
|
|
199
|
+
*
|
|
200
|
+
* @throws {Error} When the store URL is unreachable or returns an error
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* const shop = new ShopClient('https://exampleshop.com');
|
|
205
|
+
* const storeInfo = await shop.getInfo();
|
|
206
|
+
*
|
|
207
|
+
* console.log(storeInfo.name); // "Example Store"
|
|
208
|
+
* console.log(storeInfo.socialLinks.instagram); // "https://instagram.com/example"
|
|
209
|
+
* console.log(storeInfo.showcase.products); // ["product-handle-1", "product-handle-2"]
|
|
210
|
+
* console.log(storeInfo.country); // "US"
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
info: async () => {
|
|
214
|
+
try {
|
|
215
|
+
const { info } = await getInfoForStore({
|
|
216
|
+
baseUrl: context.baseUrl,
|
|
217
|
+
storeDomain: context.storeDomain,
|
|
218
|
+
validateProductExists: context.validateProductExists,
|
|
219
|
+
validateCollectionExists: context.validateCollectionExists,
|
|
220
|
+
validateLinksInBatches: context.validateLinksInBatches
|
|
221
|
+
});
|
|
222
|
+
return info;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
context.handleFetchError(error, "fetching store info", context.baseUrl);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export {
|
|
231
|
+
getInfoForStore,
|
|
232
|
+
createStoreOperations
|
|
233
|
+
};
|