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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +912 -0
  3. package/dist/checkout.d.mts +31 -0
  4. package/dist/checkout.d.ts +31 -0
  5. package/dist/checkout.js +115 -0
  6. package/dist/checkout.js.map +1 -0
  7. package/dist/checkout.mjs +7 -0
  8. package/dist/checkout.mjs.map +1 -0
  9. package/dist/chunk-2KBOKOAD.mjs +177 -0
  10. package/dist/chunk-2KBOKOAD.mjs.map +1 -0
  11. package/dist/chunk-BWKBRM2Z.mjs +136 -0
  12. package/dist/chunk-BWKBRM2Z.mjs.map +1 -0
  13. package/dist/chunk-O4BPIIQ6.mjs +503 -0
  14. package/dist/chunk-O4BPIIQ6.mjs.map +1 -0
  15. package/dist/chunk-QCTICSBE.mjs +398 -0
  16. package/dist/chunk-QCTICSBE.mjs.map +1 -0
  17. package/dist/chunk-QL5OUZGP.mjs +91 -0
  18. package/dist/chunk-QL5OUZGP.mjs.map +1 -0
  19. package/dist/chunk-WTK5HUFI.mjs +1287 -0
  20. package/dist/chunk-WTK5HUFI.mjs.map +1 -0
  21. package/dist/collections.d.mts +64 -0
  22. package/dist/collections.d.ts +64 -0
  23. package/dist/collections.js +540 -0
  24. package/dist/collections.js.map +1 -0
  25. package/dist/collections.mjs +9 -0
  26. package/dist/collections.mjs.map +1 -0
  27. package/dist/index.d.mts +233 -0
  28. package/dist/index.d.ts +233 -0
  29. package/dist/index.js +3241 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/index.mjs +702 -0
  32. package/dist/index.mjs.map +1 -0
  33. package/dist/products.d.mts +63 -0
  34. package/dist/products.d.ts +63 -0
  35. package/dist/products.js +1206 -0
  36. package/dist/products.js.map +1 -0
  37. package/dist/products.mjs +9 -0
  38. package/dist/products.mjs.map +1 -0
  39. package/dist/store-CJVUz2Yb.d.mts +608 -0
  40. package/dist/store-CJVUz2Yb.d.ts +608 -0
  41. package/dist/store.d.mts +1 -0
  42. package/dist/store.d.ts +1 -0
  43. package/dist/store.js +698 -0
  44. package/dist/store.js.map +1 -0
  45. package/dist/store.mjs +9 -0
  46. package/dist/store.mjs.map +1 -0
  47. package/dist/utils/rate-limit.d.mts +25 -0
  48. package/dist/utils/rate-limit.d.ts +25 -0
  49. package/dist/utils/rate-limit.js +203 -0
  50. package/dist/utils/rate-limit.js.map +1 -0
  51. package/dist/utils/rate-limit.mjs +11 -0
  52. package/dist/utils/rate-limit.mjs.map +1 -0
  53. package/package.json +116 -0
@@ -0,0 +1,1206 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/products.ts
31
+ var products_exports = {};
32
+ __export(products_exports, {
33
+ createProductOperations: () => createProductOperations
34
+ });
35
+ module.exports = __toCommonJS(products_exports);
36
+ var import_remeda = require("remeda");
37
+
38
+ // src/ai/enrich.ts
39
+ var import_turndown = __toESM(require("turndown"));
40
+ var import_turndown_plugin_gfm = require("turndown-plugin-gfm");
41
+
42
+ // src/utils/rate-limit.ts
43
+ var RateLimiter = class {
44
+ constructor(options) {
45
+ this.queue = [];
46
+ this.inFlight = 0;
47
+ this.refillTimer = null;
48
+ this.options = options;
49
+ this.tokens = options.maxRequestsPerInterval;
50
+ }
51
+ startRefill() {
52
+ if (this.refillTimer) return;
53
+ this.refillTimer = setInterval(() => {
54
+ this.tokens = this.options.maxRequestsPerInterval;
55
+ this.tryRun();
56
+ }, this.options.intervalMs);
57
+ if (this.refillTimer && typeof this.refillTimer.unref === "function") {
58
+ this.refillTimer.unref();
59
+ }
60
+ }
61
+ stopRefill() {
62
+ if (this.refillTimer) {
63
+ clearInterval(this.refillTimer);
64
+ this.refillTimer = null;
65
+ }
66
+ }
67
+ ensureRefillStarted() {
68
+ if (!this.refillTimer) {
69
+ this.startRefill();
70
+ }
71
+ }
72
+ configure(next) {
73
+ this.options = { ...this.options, ...next };
74
+ this.options.maxRequestsPerInterval = Math.max(
75
+ 1,
76
+ this.options.maxRequestsPerInterval
77
+ );
78
+ this.options.intervalMs = Math.max(10, this.options.intervalMs);
79
+ this.options.maxConcurrency = Math.max(1, this.options.maxConcurrency);
80
+ }
81
+ schedule(fn) {
82
+ return new Promise((resolve, reject) => {
83
+ this.ensureRefillStarted();
84
+ this.queue.push({ fn, resolve, reject });
85
+ this.tryRun();
86
+ });
87
+ }
88
+ tryRun() {
89
+ while (this.queue.length > 0 && this.inFlight < this.options.maxConcurrency && this.tokens > 0) {
90
+ const task = this.queue.shift();
91
+ this.tokens -= 1;
92
+ this.inFlight += 1;
93
+ Promise.resolve().then(task.fn).then((result) => task.resolve(result)).catch((err) => task.reject(err)).finally(() => {
94
+ this.inFlight -= 1;
95
+ setTimeout(() => this.tryRun(), 0);
96
+ });
97
+ }
98
+ }
99
+ };
100
+ var enabled = false;
101
+ var defaultOptions = {
102
+ maxRequestsPerInterval: 5,
103
+ // 5 requests
104
+ intervalMs: 1e3,
105
+ // per second
106
+ maxConcurrency: 5
107
+ // up to 5 in parallel
108
+ };
109
+ var limiter = new RateLimiter(defaultOptions);
110
+ var hostLimiters = /* @__PURE__ */ new Map();
111
+ var classLimiters = /* @__PURE__ */ new Map();
112
+ function getHost(input) {
113
+ try {
114
+ if (typeof input === "string") {
115
+ return new URL(input).host;
116
+ }
117
+ if (input instanceof URL) {
118
+ return input.host;
119
+ }
120
+ const url = input.url;
121
+ if (url) {
122
+ return new URL(url).host;
123
+ }
124
+ } catch {
125
+ }
126
+ return void 0;
127
+ }
128
+ function getHostLimiter(host) {
129
+ if (!host) return void 0;
130
+ const exact = hostLimiters.get(host);
131
+ if (exact) return exact;
132
+ for (const [key, lim] of hostLimiters.entries()) {
133
+ if (key.startsWith("*.") && host.endsWith(key.slice(2))) {
134
+ return lim;
135
+ }
136
+ }
137
+ return void 0;
138
+ }
139
+ async function rateLimitedFetch(input, init) {
140
+ var _a;
141
+ if (!enabled) {
142
+ return fetch(input, init);
143
+ }
144
+ const klass = init == null ? void 0 : init.rateLimitClass;
145
+ const byClass = klass ? classLimiters.get(klass) : void 0;
146
+ const byHost = getHostLimiter(getHost(input));
147
+ const eff = (_a = byClass != null ? byClass : byHost) != null ? _a : limiter;
148
+ return eff.schedule(() => fetch(input, init));
149
+ }
150
+
151
+ // src/ai/enrich.ts
152
+ function ensureOpenRouter(apiKey) {
153
+ const key = apiKey || process.env.OPENROUTER_API_KEY;
154
+ if (!key) {
155
+ throw new Error(
156
+ "Missing OpenRouter API key. Set OPENROUTER_API_KEY or pass apiKey."
157
+ );
158
+ }
159
+ return key;
160
+ }
161
+ function normalizeDomainToBase(domain) {
162
+ if (domain.startsWith("http://") || domain.startsWith("https://")) {
163
+ try {
164
+ const u = new URL(domain);
165
+ return `${u.protocol}//${u.hostname}`;
166
+ } catch {
167
+ return domain;
168
+ }
169
+ }
170
+ return `https://${domain}`;
171
+ }
172
+ async function fetchAjaxProduct(domain, handle) {
173
+ const base = normalizeDomainToBase(domain);
174
+ const url = `${base}/products/${handle}.js`;
175
+ const res = await rateLimitedFetch(url);
176
+ if (!res.ok) throw new Error(`Failed to fetch AJAX product: ${url}`);
177
+ const data = await res.json();
178
+ return data;
179
+ }
180
+ async function fetchProductPage(domain, handle) {
181
+ const base = normalizeDomainToBase(domain);
182
+ const url = `${base}/products/${handle}`;
183
+ const res = await rateLimitedFetch(url);
184
+ if (!res.ok) throw new Error(`Failed to fetch product page: ${url}`);
185
+ return res.text();
186
+ }
187
+ function extractMainSection(html) {
188
+ const startMatch = html.match(
189
+ /<section[^>]*id="shopify-section-template--.*?__main"[^>]*>/
190
+ );
191
+ if (!startMatch) return null;
192
+ const startIndex = html.indexOf(startMatch[0]);
193
+ if (startIndex === -1) return null;
194
+ const endIndex = html.indexOf("</section>", startIndex);
195
+ if (endIndex === -1) return null;
196
+ return html.substring(startIndex, endIndex + "</section>".length);
197
+ }
198
+ function htmlToMarkdown(html, options) {
199
+ var _a;
200
+ if (!html) return "";
201
+ const td = new import_turndown.default({
202
+ headingStyle: "atx",
203
+ codeBlockStyle: "fenced",
204
+ bulletListMarker: "-",
205
+ emDelimiter: "*",
206
+ strongDelimiter: "**",
207
+ linkStyle: "inlined"
208
+ });
209
+ const useGfm = (_a = options == null ? void 0 : options.useGfm) != null ? _a : true;
210
+ if (useGfm) {
211
+ td.use(import_turndown_plugin_gfm.gfm);
212
+ }
213
+ ["script", "style", "nav", "footer"].forEach((tag) => {
214
+ td.remove((node) => {
215
+ var _a2;
216
+ return ((_a2 = node.nodeName) == null ? void 0 : _a2.toLowerCase()) === tag;
217
+ });
218
+ });
219
+ const removeByClass = (className) => td.remove((node) => {
220
+ const cls = typeof node.getAttribute === "function" ? node.getAttribute("class") || "" : "";
221
+ return cls.split(/\s+/).includes(className);
222
+ });
223
+ [
224
+ "product-form",
225
+ "shopify-payment-button",
226
+ "shopify-payment-buttons",
227
+ "product__actions",
228
+ "product__media-wrapper",
229
+ "loox-rating",
230
+ "jdgm-widget",
231
+ "stamped-reviews"
232
+ ].forEach(removeByClass);
233
+ ["button", "input", "select", "label"].forEach((tag) => {
234
+ td.remove((node) => {
235
+ var _a2;
236
+ return ((_a2 = node.nodeName) == null ? void 0 : _a2.toLowerCase()) === tag;
237
+ });
238
+ });
239
+ ["quantity-selector", "product-atc-wrapper"].forEach(removeByClass);
240
+ return td.turndown(html);
241
+ }
242
+ async function mergeWithLLM(bodyInput, pageInput, options) {
243
+ var _a, _b;
244
+ const inputType = (_a = options == null ? void 0 : options.inputType) != null ? _a : "markdown";
245
+ const bodyLabel = inputType === "html" ? "BODY HTML" : "BODY MARKDOWN";
246
+ const pageLabel = inputType === "html" ? "PAGE HTML" : "PAGE MARKDOWN";
247
+ const prompt = (options == null ? void 0 : options.outputFormat) === "json" ? `You are extracting structured buyer-useful information from Shopify product content.
248
+
249
+ Inputs:
250
+ 1) ${bodyLabel}: ${inputType === "html" ? "Raw Shopify product body_html" : "Cleaned version of Shopify product body_html"}
251
+ 2) ${pageLabel}: ${inputType === "html" ? "Raw product page HTML (main section)" : "Extracted product page HTML converted to markdown"}
252
+
253
+ Return ONLY valid JSON (no markdown, no code fences) with this shape:
254
+ {
255
+ "title": null | string,
256
+ "description": null | string,
257
+ "materials": string[] | [],
258
+ "care": string[] | [],
259
+ "fit": null | string,
260
+ "images": null | string[],
261
+ "returnPolicy": null | string
262
+ }
263
+
264
+ Rules:
265
+ - Do not invent facts; if a field is unavailable, use null or []
266
+ - Prefer concise, factual statements
267
+ - Do NOT include product gallery/hero images in "images"; include only documentation images like size charts or measurement guides. If none, set "images": null.
268
+
269
+ ${bodyLabel}:
270
+ ${bodyInput}
271
+
272
+ ${pageLabel}:
273
+ ${pageInput}
274
+ ` : `
275
+ You are enriching a Shopify product for a modern shopping-discovery app.
276
+
277
+ Inputs:
278
+ 1) ${bodyLabel}: ${inputType === "html" ? "Raw Shopify product body_html" : "Cleaned version of Shopify product body_html"}
279
+ 2) ${pageLabel}: ${inputType === "html" ? "Raw product page HTML (main section)" : "Extracted product page HTML converted to markdown"}
280
+
281
+ Your tasks:
282
+ - Merge them into a single clean markdown document
283
+ - Remove duplicate content
284
+ - Remove product images
285
+ - Remove UI text, buttons, menus, review widgets, theme junk
286
+ - Remove product options
287
+ - Keep only available buyer-useful info: features, materials, care, fit, size chart, return policy, size chart, care instructions
288
+ - Include image of size-chart if present
289
+ - Don't include statements like information not available.
290
+ - Maintain structured headings (## Description, ## Materials, etc.)
291
+ - Output ONLY markdown (no commentary)
292
+
293
+ ${bodyLabel}:
294
+ ${bodyInput}
295
+
296
+ ${pageLabel}:
297
+ ${pageInput}
298
+ `;
299
+ const apiKey = ensureOpenRouter(options == null ? void 0 : options.apiKey);
300
+ const defaultModel = process.env.OPENROUTER_MODEL || "openai/gpt-4o-mini";
301
+ const model = (_b = options == null ? void 0 : options.model) != null ? _b : defaultModel;
302
+ const result = await callOpenRouter(model, prompt, apiKey);
303
+ if ((options == null ? void 0 : options.outputFormat) === "json") {
304
+ const cleaned = result.replace(/```json|```/g, "").trim();
305
+ const obj = safeParseJson(cleaned);
306
+ if (!obj.ok) {
307
+ throw new Error(`LLM returned invalid JSON: ${obj.error}`);
308
+ }
309
+ const schema = validateStructuredJson(obj.value);
310
+ if (!schema.ok) {
311
+ throw new Error(`LLM JSON schema invalid: ${schema.error}`);
312
+ }
313
+ const value = obj.value;
314
+ if (Array.isArray(value.images)) {
315
+ const filtered = value.images.filter((url) => {
316
+ if (typeof url !== "string") return false;
317
+ const u = url.toLowerCase();
318
+ const productPatterns = [
319
+ "cdn.shopify.com",
320
+ "/products/",
321
+ "%2Fproducts%2F",
322
+ "_large",
323
+ "_grande",
324
+ "_1024x1024",
325
+ "_2048x"
326
+ ];
327
+ const looksLikeProductImage = productPatterns.some(
328
+ (p) => u.includes(p)
329
+ );
330
+ return !looksLikeProductImage;
331
+ });
332
+ value.images = filtered.length > 0 ? filtered : null;
333
+ }
334
+ return JSON.stringify(value);
335
+ }
336
+ return result;
337
+ }
338
+ function safeParseJson(input) {
339
+ try {
340
+ const v = JSON.parse(input);
341
+ return { ok: true, value: v };
342
+ } catch (err) {
343
+ return { ok: false, error: (err == null ? void 0 : err.message) || "Failed to parse JSON" };
344
+ }
345
+ }
346
+ function validateStructuredJson(obj) {
347
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
348
+ return { ok: false, error: "Top-level must be a JSON object" };
349
+ }
350
+ const o = obj;
351
+ if ("title" in o && !(o.title === null || typeof o.title === "string")) {
352
+ return { ok: false, error: "title must be null or string" };
353
+ }
354
+ if ("description" in o && !(o.description === null || typeof o.description === "string")) {
355
+ return { ok: false, error: "description must be null or string" };
356
+ }
357
+ if ("fit" in o && !(o.fit === null || typeof o.fit === "string")) {
358
+ return { ok: false, error: "fit must be null or string" };
359
+ }
360
+ if ("returnPolicy" in o && !(o.returnPolicy === null || typeof o.returnPolicy === "string")) {
361
+ return { ok: false, error: "returnPolicy must be null or string" };
362
+ }
363
+ const validateStringArray = (arr, field) => {
364
+ if (!Array.isArray(arr))
365
+ return { ok: false, error: `${field} must be an array` };
366
+ for (const item of arr) {
367
+ if (typeof item !== "string")
368
+ return { ok: false, error: `${field} items must be strings` };
369
+ }
370
+ return { ok: true };
371
+ };
372
+ if ("materials" in o) {
373
+ const res = validateStringArray(o.materials, "materials");
374
+ if (!res.ok) return res;
375
+ }
376
+ if ("care" in o) {
377
+ const res = validateStringArray(o.care, "care");
378
+ if (!res.ok) return res;
379
+ }
380
+ if ("images" in o) {
381
+ if (!(o.images === null || Array.isArray(o.images))) {
382
+ return { ok: false, error: "images must be null or an array" };
383
+ }
384
+ if (Array.isArray(o.images)) {
385
+ const res = validateStringArray(o.images, "images");
386
+ if (!res.ok) return res;
387
+ }
388
+ }
389
+ return { ok: true };
390
+ }
391
+ async function callOpenRouter(model, prompt, apiKey) {
392
+ var _a, _b, _c;
393
+ if (process.env.OPENROUTER_OFFLINE === "1") {
394
+ return mockOpenRouterResponse(prompt);
395
+ }
396
+ const headers = {
397
+ "Content-Type": "application/json",
398
+ Authorization: `Bearer ${apiKey}`
399
+ };
400
+ const referer = process.env.OPENROUTER_SITE_URL || process.env.SITE_URL;
401
+ const title = process.env.OPENROUTER_APP_TITLE || "Shop Search";
402
+ if (referer) headers["HTTP-Referer"] = referer;
403
+ if (title) headers["X-Title"] = title;
404
+ const buildPayload = (m) => ({
405
+ model: m,
406
+ messages: [{ role: "user", content: prompt }],
407
+ temperature: 0.2
408
+ });
409
+ const base = (process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1").replace(/\/$/, "");
410
+ const endpoints = [`${base}/chat/completions`];
411
+ const fallbackEnv = (process.env.OPENROUTER_FALLBACK_MODELS || "").split(",").map((s) => s.trim()).filter(Boolean);
412
+ const defaultModel = process.env.OPENROUTER_MODEL || "openai/gpt-4o-mini";
413
+ const modelsToTry = Array.from(
414
+ /* @__PURE__ */ new Set([model, ...fallbackEnv, defaultModel])
415
+ ).filter(Boolean);
416
+ let lastErrorText = "";
417
+ for (const m of modelsToTry) {
418
+ for (const url of endpoints) {
419
+ try {
420
+ const controller = new AbortController();
421
+ const timeout = setTimeout(() => controller.abort(), 15e3);
422
+ const response = await rateLimitedFetch(url, {
423
+ method: "POST",
424
+ headers,
425
+ body: JSON.stringify(buildPayload(m)),
426
+ signal: controller.signal
427
+ });
428
+ clearTimeout(timeout);
429
+ if (!response.ok) {
430
+ const text = await response.text();
431
+ lastErrorText = text || `${url}: HTTP ${response.status}`;
432
+ await new Promise((r) => setTimeout(r, 300));
433
+ continue;
434
+ }
435
+ const data = await response.json();
436
+ const content = (_c = (_b = (_a = data == null ? void 0 : data.choices) == null ? void 0 : _a[0]) == null ? void 0 : _b.message) == null ? void 0 : _c.content;
437
+ if (typeof content === "string") return content;
438
+ lastErrorText = JSON.stringify(data);
439
+ await new Promise((r) => setTimeout(r, 200));
440
+ } catch (err) {
441
+ lastErrorText = `${url}: ${(err == null ? void 0 : err.message) || String(err)}`;
442
+ await new Promise((r) => setTimeout(r, 200));
443
+ }
444
+ }
445
+ }
446
+ throw new Error(`OpenRouter request failed: ${lastErrorText}`);
447
+ }
448
+ function mockOpenRouterResponse(prompt) {
449
+ const p = prompt.toLowerCase();
450
+ if (p.includes("return only valid json") && p.includes('"audience":')) {
451
+ return JSON.stringify({
452
+ audience: "generic",
453
+ vertical: "clothing",
454
+ category: null,
455
+ subCategory: null
456
+ });
457
+ }
458
+ if (p.includes("return only valid json") && p.includes('"materials":')) {
459
+ return JSON.stringify({
460
+ title: null,
461
+ description: null,
462
+ materials: [],
463
+ care: [],
464
+ fit: null,
465
+ images: null,
466
+ returnPolicy: null
467
+ });
468
+ }
469
+ return [
470
+ "## Description",
471
+ "Offline merge of product body and page.",
472
+ "",
473
+ "## Materials",
474
+ "- Not available"
475
+ ].join("\n");
476
+ }
477
+ async function enrichProduct(domain, handle, options) {
478
+ var _a;
479
+ const ajaxProduct = await fetchAjaxProduct(domain, handle);
480
+ const bodyHtml = ajaxProduct.description || "";
481
+ const pageHtml = await fetchProductPage(domain, handle);
482
+ const extractedHtml = extractMainSection(pageHtml);
483
+ const inputType = (_a = options == null ? void 0 : options.inputType) != null ? _a : "markdown";
484
+ const bodyInput = inputType === "html" ? bodyHtml : htmlToMarkdown(bodyHtml, { useGfm: options == null ? void 0 : options.useGfm });
485
+ const pageInput = inputType === "html" ? extractedHtml || pageHtml : htmlToMarkdown(extractedHtml, { useGfm: options == null ? void 0 : options.useGfm });
486
+ const mergedMarkdown = await mergeWithLLM(bodyInput, pageInput, {
487
+ apiKey: options == null ? void 0 : options.apiKey,
488
+ inputType,
489
+ model: options == null ? void 0 : options.model,
490
+ outputFormat: options == null ? void 0 : options.outputFormat
491
+ });
492
+ if ((options == null ? void 0 : options.outputFormat) === "json") {
493
+ try {
494
+ const obj = JSON.parse(mergedMarkdown);
495
+ if (obj && Array.isArray(obj.images)) {
496
+ const productImageCandidates = [];
497
+ if (ajaxProduct.featured_image) {
498
+ productImageCandidates.push(String(ajaxProduct.featured_image));
499
+ }
500
+ if (Array.isArray(ajaxProduct.images)) {
501
+ for (const img of ajaxProduct.images) {
502
+ if (typeof img === "string" && img.length > 0) {
503
+ productImageCandidates.push(img);
504
+ }
505
+ }
506
+ }
507
+ if (Array.isArray(ajaxProduct.media)) {
508
+ for (const m of ajaxProduct.media) {
509
+ if (m == null ? void 0 : m.src) productImageCandidates.push(String(m.src));
510
+ }
511
+ }
512
+ if (Array.isArray(ajaxProduct.variants)) {
513
+ for (const v of ajaxProduct.variants) {
514
+ const fi = v == null ? void 0 : v.featured_image;
515
+ if (fi == null ? void 0 : fi.src) productImageCandidates.push(String(fi.src));
516
+ }
517
+ }
518
+ const productSet = new Set(
519
+ productImageCandidates.map((u) => String(u).toLowerCase())
520
+ );
521
+ const filtered = obj.images.filter((url) => {
522
+ if (typeof url !== "string") return false;
523
+ const u = url.toLowerCase();
524
+ if (productSet.has(u)) return false;
525
+ const productPatterns = [
526
+ "cdn.shopify.com",
527
+ "/products/",
528
+ "%2Fproducts%2F",
529
+ "_large",
530
+ "_grande",
531
+ "_1024x1024",
532
+ "_2048x"
533
+ ];
534
+ const looksLikeProductImage = productPatterns.some(
535
+ (p) => u.includes(p)
536
+ );
537
+ return !looksLikeProductImage;
538
+ });
539
+ obj.images = filtered.length > 0 ? filtered : null;
540
+ const sanitized = JSON.stringify(obj);
541
+ return {
542
+ bodyHtml,
543
+ pageHtml,
544
+ extractedMainHtml: extractedHtml || "",
545
+ mergedMarkdown: sanitized
546
+ };
547
+ }
548
+ } catch {
549
+ }
550
+ }
551
+ return {
552
+ bodyHtml,
553
+ pageHtml,
554
+ extractedMainHtml: extractedHtml || "",
555
+ mergedMarkdown
556
+ };
557
+ }
558
+ async function classifyProduct(productContent, options) {
559
+ var _a;
560
+ const apiKey = ensureOpenRouter(options == null ? void 0 : options.apiKey);
561
+ const defaultModel = process.env.OPENROUTER_MODEL || "openai/gpt-4o-mini";
562
+ const model = (_a = options == null ? void 0 : options.model) != null ? _a : defaultModel;
563
+ const prompt = `Classify the following product using a three-tiered hierarchy:
564
+
565
+ Product Content:
566
+ ${productContent}
567
+
568
+ Classification Rules:
569
+ 1. First determine the vertical (main product category)
570
+ 2. Then determine the category (specific type within that vertical)
571
+ 3. Finally determine the subCategory (sub-type within that category)
572
+
573
+ Vertical must be one of: clothing, beauty, accessories, home-decor, food-and-beverages
574
+ Audience must be one of: adult_male, adult_female, kid_male, kid_female, generic
575
+
576
+ Hierarchy Examples:
577
+ - Clothing \u2192 tops \u2192 t-shirts
578
+ - Clothing \u2192 footwear \u2192 sneakers
579
+ - Beauty \u2192 skincare \u2192 moisturizers
580
+ - Accessories \u2192 bags \u2192 backpacks
581
+ - Home-decor \u2192 furniture \u2192 chairs
582
+ - Food-and-beverages \u2192 snacks \u2192 chips
583
+
584
+ IMPORTANT CONSTRAINTS:
585
+ - Category must be relevant to the chosen vertical
586
+ - subCategory must be relevant to both vertical and category
587
+ - subCategory must be a single word or hyphenated words (no spaces)
588
+ - subCategory should NOT be material (e.g., "cotton", "leather") or color (e.g., "red", "blue")
589
+ - Focus on product type/function, not attributes
590
+
591
+ If you're not confident about category or sub-category, you can leave them optional.
592
+
593
+ Return ONLY valid JSON (no markdown, no code fences) with keys:
594
+ {
595
+ "audience": "adult_male" | "adult_female" | "kid_male" | "kid_female" | "generic",
596
+ "vertical": "clothing" | "beauty" | "accessories" | "home-decor" | "food-and-beverages",
597
+ "category": null | string,
598
+ "subCategory": null | string
599
+ }`;
600
+ const raw = await callOpenRouter(model, prompt, apiKey);
601
+ const cleaned = raw.replace(/```json|```/g, "").trim();
602
+ const parsed = safeParseJson(cleaned);
603
+ if (!parsed.ok) {
604
+ throw new Error(`LLM returned invalid JSON: ${parsed.error}`);
605
+ }
606
+ const validated = validateClassification(parsed.value);
607
+ if (!validated.ok) {
608
+ throw new Error(`LLM JSON schema invalid: ${validated.error}`);
609
+ }
610
+ return validated.value;
611
+ }
612
+ function validateClassification(obj) {
613
+ var _a, _b;
614
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
615
+ return { ok: false, error: "Top-level must be a JSON object" };
616
+ }
617
+ const o = obj;
618
+ const audienceValues = [
619
+ "adult_male",
620
+ "adult_female",
621
+ "kid_male",
622
+ "kid_female",
623
+ "generic"
624
+ ];
625
+ if (typeof o.audience !== "string" || !audienceValues.includes(o.audience)) {
626
+ return {
627
+ ok: false,
628
+ error: "audience must be one of: adult_male, adult_female, kid_male, kid_female, generic"
629
+ };
630
+ }
631
+ const verticalValues = [
632
+ "clothing",
633
+ "beauty",
634
+ "accessories",
635
+ "home-decor",
636
+ "food-and-beverages"
637
+ ];
638
+ if (typeof o.vertical !== "string" || !verticalValues.includes(o.vertical)) {
639
+ return {
640
+ ok: false,
641
+ error: "vertical must be one of: clothing, beauty, accessories, home-decor, food-and-beverages"
642
+ };
643
+ }
644
+ if ("category" in o && !(o.category === null || typeof o.category === "string")) {
645
+ return { ok: false, error: "category must be null or string" };
646
+ }
647
+ if ("subCategory" in o && !(o.subCategory === null || typeof o.subCategory === "string")) {
648
+ return { ok: false, error: "subCategory must be null or string" };
649
+ }
650
+ if (typeof o.subCategory === "string") {
651
+ const sc = o.subCategory.trim();
652
+ if (!/^[A-Za-z0-9-]+$/.test(sc)) {
653
+ return {
654
+ ok: false,
655
+ error: "subCategory must be single word or hyphenated, no spaces"
656
+ };
657
+ }
658
+ }
659
+ return {
660
+ ok: true,
661
+ value: {
662
+ audience: o.audience,
663
+ vertical: o.vertical,
664
+ category: typeof o.category === "string" ? o.category : (_a = o.category) != null ? _a : null,
665
+ subCategory: typeof o.subCategory === "string" ? o.subCategory : (_b = o.subCategory) != null ? _b : null
666
+ }
667
+ };
668
+ }
669
+ async function generateSEOContent(product, options) {
670
+ var _a;
671
+ const apiKey = ensureOpenRouter(options == null ? void 0 : options.apiKey);
672
+ const defaultModel = process.env.OPENROUTER_MODEL || "openai/gpt-4o-mini";
673
+ const model = (_a = options == null ? void 0 : options.model) != null ? _a : defaultModel;
674
+ if (process.env.OPENROUTER_OFFLINE === "1") {
675
+ const baseTags = Array.isArray(product.tags) ? product.tags.slice(0, 6) : [];
676
+ const titlePart = product.title.trim().slice(0, 50);
677
+ const vendorPart = (product.vendor || "").trim();
678
+ const pricePart = typeof product.price === "number" ? `$${product.price}` : "";
679
+ const metaTitle = vendorPart ? `${titlePart} | ${vendorPart}` : titlePart;
680
+ const metaDescription = `Discover ${product.title}. ${pricePart ? `Priced at ${pricePart}. ` : ""}Crafted to delight customers with quality and style.`.slice(
681
+ 0,
682
+ 160
683
+ );
684
+ const shortDescription = `${product.title} \u2014 ${vendorPart || "Premium"} quality, designed to impress.`;
685
+ const longDescription = product.description || `Introducing ${product.title}, combining performance and style for everyday use.`;
686
+ const marketingCopy = `Get ${product.title} today${pricePart ? ` for ${pricePart}` : ""}. Limited availability \u2014 don\u2019t miss out!`;
687
+ const res = {
688
+ metaTitle,
689
+ metaDescription,
690
+ shortDescription,
691
+ longDescription,
692
+ tags: baseTags.length ? baseTags : ["new", "featured", "popular"],
693
+ marketingCopy
694
+ };
695
+ const validated2 = validateSEOContent(res);
696
+ if (!validated2.ok)
697
+ throw new Error(`Offline SEO content invalid: ${validated2.error}`);
698
+ return validated2.value;
699
+ }
700
+ const prompt = `Generate SEO-optimized content for this product:
701
+
702
+ Title: ${product.title}
703
+ Description: ${product.description || "N/A"}
704
+ Vendor: ${product.vendor || "N/A"}
705
+ Price: ${typeof product.price === "number" ? `$${product.price}` : "N/A"}
706
+ Tags: ${Array.isArray(product.tags) && product.tags.length ? product.tags.join(", ") : "N/A"}
707
+
708
+ Create compelling, SEO-friendly content that will help this product rank well and convert customers.
709
+
710
+ Return ONLY valid JSON (no markdown, no code fences) with keys: {
711
+ "metaTitle": string,
712
+ "metaDescription": string,
713
+ "shortDescription": string,
714
+ "longDescription": string,
715
+ "tags": string[],
716
+ "marketingCopy": string
717
+ }`;
718
+ const raw = await callOpenRouter(model, prompt, apiKey);
719
+ const cleaned = raw.replace(/```json|```/g, "").trim();
720
+ const parsed = safeParseJson(cleaned);
721
+ if (!parsed.ok) {
722
+ throw new Error(`LLM returned invalid JSON: ${parsed.error}`);
723
+ }
724
+ const validated = validateSEOContent(parsed.value);
725
+ if (!validated.ok) {
726
+ throw new Error(`LLM JSON schema invalid: ${validated.error}`);
727
+ }
728
+ return validated.value;
729
+ }
730
+ function validateSEOContent(obj) {
731
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
732
+ return { ok: false, error: "Top-level must be a JSON object" };
733
+ }
734
+ const o = obj;
735
+ const requiredStrings = [
736
+ "metaTitle",
737
+ "metaDescription",
738
+ "shortDescription",
739
+ "longDescription",
740
+ "marketingCopy"
741
+ ];
742
+ for (const key of requiredStrings) {
743
+ if (typeof o[key] !== "string" || !o[key].trim()) {
744
+ return { ok: false, error: `${key} must be a non-empty string` };
745
+ }
746
+ }
747
+ if (!Array.isArray(o.tags)) {
748
+ return { ok: false, error: "tags must be an array" };
749
+ }
750
+ for (const t of o.tags) {
751
+ if (typeof t !== "string")
752
+ return { ok: false, error: "tags items must be strings" };
753
+ }
754
+ return {
755
+ ok: true,
756
+ value: {
757
+ metaTitle: String(o.metaTitle),
758
+ metaDescription: String(o.metaDescription),
759
+ shortDescription: String(o.shortDescription),
760
+ longDescription: String(o.longDescription),
761
+ tags: o.tags,
762
+ marketingCopy: String(o.marketingCopy)
763
+ }
764
+ };
765
+ }
766
+
767
+ // src/utils/func.ts
768
+ var import_tldts = require("tldts");
769
+ function formatPrice(amountInCents, currency) {
770
+ try {
771
+ return new Intl.NumberFormat(void 0, {
772
+ style: "currency",
773
+ currency
774
+ }).format((amountInCents || 0) / 100);
775
+ } catch {
776
+ const val = (amountInCents || 0) / 100;
777
+ return `${val} ${currency}`;
778
+ }
779
+ }
780
+
781
+ // src/products.ts
782
+ function createProductOperations(baseUrl, storeDomain, fetchProducts, productsDto, productDto, getStoreInfo, findProduct) {
783
+ function applyCurrencyOverride(product, currency) {
784
+ var _a, _b, _c, _d, _e, _f;
785
+ const priceMin = (_b = (_a = product.priceMin) != null ? _a : product.price) != null ? _b : 0;
786
+ const priceMax = (_d = (_c = product.priceMax) != null ? _c : product.price) != null ? _d : 0;
787
+ const compareAtMin = (_f = (_e = product.compareAtPriceMin) != null ? _e : product.compareAtPrice) != null ? _f : 0;
788
+ return {
789
+ ...product,
790
+ currency,
791
+ localizedPricing: {
792
+ currency,
793
+ priceFormatted: formatPrice(priceMin, currency),
794
+ priceMinFormatted: formatPrice(priceMin, currency),
795
+ priceMaxFormatted: formatPrice(priceMax, currency),
796
+ compareAtPriceFormatted: formatPrice(compareAtMin, currency)
797
+ }
798
+ };
799
+ }
800
+ function maybeOverrideProductsCurrency(products, currency) {
801
+ if (!products || !currency) return products;
802
+ return products.map((p) => applyCurrencyOverride(p, currency));
803
+ }
804
+ const operations = {
805
+ /**
806
+ * Fetches all products from the store across all pages.
807
+ *
808
+ * @returns {Promise<Product[] | null>} Array of all products or null if error occurs
809
+ *
810
+ * @throws {Error} When there's a network error or API failure
811
+ *
812
+ * @example
813
+ * ```typescript
814
+ * const shop = new ShopClient('https://exampleshop.com');
815
+ * const allProducts = await shop.products.all();
816
+ *
817
+ * console.log(`Found ${allProducts?.length} products`);
818
+ * allProducts?.forEach(product => {
819
+ * console.log(product.title, product.price);
820
+ * });
821
+ * ```
822
+ */
823
+ all: async (options) => {
824
+ const limit = 250;
825
+ const allProducts = [];
826
+ async function fetchAll() {
827
+ let currentPage = 1;
828
+ while (true) {
829
+ const products = await fetchProducts(currentPage, limit);
830
+ if (!products || products.length === 0 || products.length < limit) {
831
+ if (products && products.length > 0) {
832
+ allProducts.push(...products);
833
+ }
834
+ break;
835
+ }
836
+ allProducts.push(...products);
837
+ currentPage++;
838
+ }
839
+ return allProducts;
840
+ }
841
+ try {
842
+ const products = await fetchAll();
843
+ return maybeOverrideProductsCurrency(products, options == null ? void 0 : options.currency);
844
+ } catch (error) {
845
+ console.error("Failed to fetch all products:", storeDomain, error);
846
+ throw error;
847
+ }
848
+ },
849
+ /**
850
+ * Fetches products with pagination support.
851
+ *
852
+ * @param options - Pagination options
853
+ * @param options.page - Page number (default: 1)
854
+ * @param options.limit - Number of products per page (default: 250, max: 250)
855
+ *
856
+ * @returns {Promise<Product[] | null>} Array of products for the specified page or null if error occurs
857
+ *
858
+ * @throws {Error} When there's a network error or API failure
859
+ *
860
+ * @example
861
+ * ```typescript
862
+ * const shop = new ShopClient('https://example.myshopify.com');
863
+ *
864
+ * // Get first page with default limit (250)
865
+ * const firstPage = await shop.products.paginated();
866
+ *
867
+ * // Get second page with custom limit
868
+ * const secondPage = await shop.products.paginated({ page: 2, limit: 50 });
869
+ * ```
870
+ */
871
+ paginated: async (options) => {
872
+ var _a, _b;
873
+ const page = (_a = options == null ? void 0 : options.page) != null ? _a : 1;
874
+ const limit = Math.min((_b = options == null ? void 0 : options.limit) != null ? _b : 250, 250);
875
+ const url = `${baseUrl}products.json?limit=${limit}&page=${page}`;
876
+ try {
877
+ const response = await rateLimitedFetch(url);
878
+ if (!response.ok) {
879
+ console.error(
880
+ `HTTP error! status: ${response.status} for ${storeDomain} page ${page}`
881
+ );
882
+ throw new Error(`HTTP error! status: ${response.status}`);
883
+ }
884
+ const data = await response.json();
885
+ if (data.products.length === 0) {
886
+ return [];
887
+ }
888
+ const normalized = productsDto(data.products);
889
+ return maybeOverrideProductsCurrency(normalized, options == null ? void 0 : options.currency);
890
+ } catch (error) {
891
+ console.error(
892
+ `Error fetching products for ${storeDomain} page ${page} with limit ${limit}:`,
893
+ error
894
+ );
895
+ return null;
896
+ }
897
+ },
898
+ /**
899
+ * Finds a specific product by its handle.
900
+ *
901
+ * @param productHandle - The product handle (URL slug) to search for
902
+ *
903
+ * @returns {Promise<Product | null>} The product if found, null if not found
904
+ *
905
+ * @throws {Error} When the handle is invalid or there's a network error
906
+ *
907
+ * @example
908
+ * ```typescript
909
+ * const shop = new ShopClient('https://exampleshop.com');
910
+ *
911
+ * // Find product by handle
912
+ * const product = await shop.products.find('awesome-t-shirt');
913
+ *
914
+ * if (product) {
915
+ * console.log(product.title, product.price);
916
+ * console.log('Available variants:', product.variants.length);
917
+ * }
918
+ *
919
+ * // Handle with query string
920
+ * const productWithVariant = await shop.products.find('t-shirt?variant=123');
921
+ * ```
922
+ */
923
+ find: async (productHandle, options) => {
924
+ var _a, _b;
925
+ if (!productHandle || typeof productHandle !== "string") {
926
+ throw new Error("Product handle is required and must be a string");
927
+ }
928
+ try {
929
+ let qs = null;
930
+ if (productHandle.includes("?")) {
931
+ const parts = productHandle.split("?");
932
+ const handlePart = (_a = parts[0]) != null ? _a : productHandle;
933
+ const qsPart = (_b = parts[1]) != null ? _b : null;
934
+ productHandle = handlePart;
935
+ qs = qsPart;
936
+ }
937
+ const sanitizedHandle = productHandle.trim().replace(/[^a-zA-Z0-9\-_]/g, "");
938
+ if (!sanitizedHandle) {
939
+ throw new Error("Invalid product handle format");
940
+ }
941
+ if (sanitizedHandle.length > 255) {
942
+ throw new Error("Product handle is too long");
943
+ }
944
+ let finalHandle = sanitizedHandle;
945
+ try {
946
+ const htmlResp = await rateLimitedFetch(
947
+ `${baseUrl}products/${encodeURIComponent(sanitizedHandle)}`
948
+ );
949
+ if (htmlResp.ok) {
950
+ const finalUrl = htmlResp.url;
951
+ if (finalUrl) {
952
+ const pathname = new URL(finalUrl).pathname.replace(/\/$/, "");
953
+ const parts = pathname.split("/").filter(Boolean);
954
+ const idx = parts.indexOf("products");
955
+ const maybeHandle = idx >= 0 ? parts[idx + 1] : void 0;
956
+ if (typeof maybeHandle === "string" && maybeHandle.length) {
957
+ finalHandle = maybeHandle;
958
+ }
959
+ }
960
+ }
961
+ } catch {
962
+ }
963
+ const url = `${baseUrl}products/${encodeURIComponent(finalHandle)}.js${qs ? `?${qs}` : ""}`;
964
+ const response = await rateLimitedFetch(url);
965
+ if (!response.ok) {
966
+ if (response.status === 404) {
967
+ return null;
968
+ }
969
+ throw new Error(`HTTP error! status: ${response.status}`);
970
+ }
971
+ const product = await response.json();
972
+ const productData = productDto(product);
973
+ return (options == null ? void 0 : options.currency) ? applyCurrencyOverride(productData, options.currency) : productData;
974
+ } catch (error) {
975
+ if (error instanceof Error) {
976
+ console.error(
977
+ `Error fetching product ${productHandle}:`,
978
+ baseUrl,
979
+ error.message
980
+ );
981
+ }
982
+ throw error;
983
+ }
984
+ },
985
+ /**
986
+ * Enrich a product by generating merged markdown from body_html and product page.
987
+ * Adds `enriched_content` to the returned product.
988
+ */
989
+ enriched: async (productHandle, options) => {
990
+ if (!productHandle || typeof productHandle !== "string") {
991
+ throw new Error("Product handle is required and must be a string");
992
+ }
993
+ const apiKey = (options == null ? void 0 : options.apiKey) || process.env.OPENROUTER_API_KEY;
994
+ if (!apiKey) {
995
+ throw new Error(
996
+ "Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY."
997
+ );
998
+ }
999
+ const baseProduct = await operations.find(productHandle);
1000
+ if (!baseProduct) {
1001
+ return null;
1002
+ }
1003
+ const handle = baseProduct.handle;
1004
+ const enriched = await enrichProduct(storeDomain, handle, {
1005
+ apiKey,
1006
+ useGfm: options == null ? void 0 : options.useGfm,
1007
+ inputType: options == null ? void 0 : options.inputType,
1008
+ model: options == null ? void 0 : options.model,
1009
+ outputFormat: options == null ? void 0 : options.outputFormat
1010
+ });
1011
+ return {
1012
+ ...baseProduct,
1013
+ enriched_content: enriched.mergedMarkdown
1014
+ };
1015
+ },
1016
+ classify: async (productHandle, options) => {
1017
+ if (!productHandle || typeof productHandle !== "string") {
1018
+ throw new Error("Product handle is required and must be a string");
1019
+ }
1020
+ const apiKey = (options == null ? void 0 : options.apiKey) || process.env.OPENROUTER_API_KEY;
1021
+ if (!apiKey) {
1022
+ throw new Error(
1023
+ "Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY."
1024
+ );
1025
+ }
1026
+ const enrichedProduct = await operations.enriched(productHandle, {
1027
+ apiKey,
1028
+ inputType: "html",
1029
+ model: options == null ? void 0 : options.model,
1030
+ outputFormat: "json"
1031
+ });
1032
+ if (!enrichedProduct || !enrichedProduct.enriched_content) return null;
1033
+ let productContent = enrichedProduct.enriched_content;
1034
+ try {
1035
+ const obj = JSON.parse(enrichedProduct.enriched_content);
1036
+ const lines = [];
1037
+ if (obj.title && typeof obj.title === "string")
1038
+ lines.push(`Title: ${obj.title}`);
1039
+ if (obj.description && typeof obj.description === "string")
1040
+ lines.push(`Description: ${obj.description}`);
1041
+ if (Array.isArray(obj.materials) && obj.materials.length)
1042
+ lines.push(`Materials: ${obj.materials.join(", ")}`);
1043
+ if (Array.isArray(obj.care) && obj.care.length)
1044
+ lines.push(`Care: ${obj.care.join(", ")}`);
1045
+ if (obj.fit && typeof obj.fit === "string")
1046
+ lines.push(`Fit: ${obj.fit}`);
1047
+ if (obj.returnPolicy && typeof obj.returnPolicy === "string")
1048
+ lines.push(`ReturnPolicy: ${obj.returnPolicy}`);
1049
+ productContent = lines.join("\n");
1050
+ } catch {
1051
+ }
1052
+ const classification = await classifyProduct(productContent, {
1053
+ apiKey,
1054
+ model: options == null ? void 0 : options.model
1055
+ });
1056
+ return classification;
1057
+ },
1058
+ generateSEOContent: async (productHandle, options) => {
1059
+ if (!productHandle || typeof productHandle !== "string") {
1060
+ throw new Error("Product handle is required and must be a string");
1061
+ }
1062
+ const apiKey = (options == null ? void 0 : options.apiKey) || process.env.OPENROUTER_API_KEY;
1063
+ if (!apiKey) {
1064
+ throw new Error(
1065
+ "Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY."
1066
+ );
1067
+ }
1068
+ const baseProduct = await operations.find(productHandle);
1069
+ if (!baseProduct) return null;
1070
+ const payload = {
1071
+ title: baseProduct.title,
1072
+ description: baseProduct.bodyHtml || void 0,
1073
+ vendor: baseProduct.vendor,
1074
+ price: baseProduct.price,
1075
+ tags: baseProduct.tags
1076
+ };
1077
+ const seo = await generateSEOContent(payload, {
1078
+ apiKey,
1079
+ model: options == null ? void 0 : options.model
1080
+ });
1081
+ return seo;
1082
+ },
1083
+ /**
1084
+ * Fetches products that are showcased/featured on the store's homepage.
1085
+ *
1086
+ * @returns {Promise<Product[]>} Array of showcased products found on the homepage
1087
+ *
1088
+ * @throws {Error} When there's a network error or API failure
1089
+ *
1090
+ * @example
1091
+ * ```typescript
1092
+ * const shop = new ShopClient('https://exampleshop.com');
1093
+ * const showcasedProducts = await shop.products.showcased();
1094
+ *
1095
+ * console.log(`Found ${showcasedProducts.length} showcased products`);
1096
+ * showcasedProducts.forEach(product => {
1097
+ * console.log(`Featured: ${product.title} - ${product.price}`);
1098
+ * });
1099
+ * ```
1100
+ */
1101
+ showcased: async () => {
1102
+ const storeInfo = await getStoreInfo();
1103
+ const products = await Promise.all(
1104
+ storeInfo.showcase.products.map(
1105
+ (productHandle) => findProduct(productHandle)
1106
+ )
1107
+ );
1108
+ return (0, import_remeda.filter)(products, import_remeda.isNonNullish);
1109
+ },
1110
+ /**
1111
+ * Creates a filter map of variant options and their distinct values from all products.
1112
+ *
1113
+ * @returns {Promise<Record<string, string[]> | null>} Map of option names to their distinct values or null if error occurs
1114
+ *
1115
+ * @throws {Error} When there's a network error or API failure
1116
+ *
1117
+ * @example
1118
+ * ```typescript
1119
+ * const shop = new ShopClient('https://exampleshop.com');
1120
+ * const filters = await shop.products.filter();
1121
+ *
1122
+ * console.log('Available filters:', filters);
1123
+ * // Output: { "Size": ["S", "M", "L", "XL"], "Color": ["Red", "Blue", "Green"] }
1124
+ *
1125
+ * // Use filters for UI components
1126
+ * Object.entries(filters || {}).forEach(([optionName, values]) => {
1127
+ * console.log(`${optionName}: ${values.join(', ')}`);
1128
+ * });
1129
+ * ```
1130
+ */
1131
+ filter: async () => {
1132
+ try {
1133
+ const products = await operations.all();
1134
+ if (!products || products.length === 0) {
1135
+ return {};
1136
+ }
1137
+ const filterMap = {};
1138
+ products.forEach((product) => {
1139
+ if (product.variants && product.variants.length > 0) {
1140
+ if (product.options && product.options.length > 0) {
1141
+ product.options.forEach((option) => {
1142
+ const lowercaseOptionName = option.name.toLowerCase();
1143
+ if (!filterMap[lowercaseOptionName]) {
1144
+ filterMap[lowercaseOptionName] = /* @__PURE__ */ new Set();
1145
+ }
1146
+ option.values.forEach((value) => {
1147
+ const trimmed = value == null ? void 0 : value.trim();
1148
+ if (trimmed) {
1149
+ let set = filterMap[lowercaseOptionName];
1150
+ if (!set) {
1151
+ set = /* @__PURE__ */ new Set();
1152
+ filterMap[lowercaseOptionName] = set;
1153
+ }
1154
+ set.add(trimmed.toLowerCase());
1155
+ }
1156
+ });
1157
+ });
1158
+ }
1159
+ product.variants.forEach((variant) => {
1160
+ var _a, _b, _c, _d, _e, _f;
1161
+ if (variant.option1) {
1162
+ const optionName = (((_b = (_a = product.options) == null ? void 0 : _a[0]) == null ? void 0 : _b.name) || "Option 1").toLowerCase();
1163
+ let set1 = filterMap[optionName];
1164
+ if (!set1) {
1165
+ set1 = /* @__PURE__ */ new Set();
1166
+ filterMap[optionName] = set1;
1167
+ }
1168
+ set1.add(variant.option1.trim().toLowerCase());
1169
+ }
1170
+ if (variant.option2) {
1171
+ const optionName = (((_d = (_c = product.options) == null ? void 0 : _c[1]) == null ? void 0 : _d.name) || "Option 2").toLowerCase();
1172
+ let set2 = filterMap[optionName];
1173
+ if (!set2) {
1174
+ set2 = /* @__PURE__ */ new Set();
1175
+ filterMap[optionName] = set2;
1176
+ }
1177
+ set2.add(variant.option2.trim().toLowerCase());
1178
+ }
1179
+ if (variant.option3) {
1180
+ const optionName = (((_f = (_e = product.options) == null ? void 0 : _e[2]) == null ? void 0 : _f.name) || "Option 3").toLowerCase();
1181
+ if (!filterMap[optionName]) {
1182
+ filterMap[optionName] = /* @__PURE__ */ new Set();
1183
+ }
1184
+ filterMap[optionName].add(variant.option3.trim().toLowerCase());
1185
+ }
1186
+ });
1187
+ }
1188
+ });
1189
+ const result = {};
1190
+ Object.entries(filterMap).forEach(([optionName, valueSet]) => {
1191
+ result[optionName] = Array.from(valueSet).sort();
1192
+ });
1193
+ return result;
1194
+ } catch (error) {
1195
+ console.error("Failed to create product filters:", storeDomain, error);
1196
+ throw error;
1197
+ }
1198
+ }
1199
+ };
1200
+ return operations;
1201
+ }
1202
+ // Annotate the CommonJS export names for ESM import in node:
1203
+ 0 && (module.exports = {
1204
+ createProductOperations
1205
+ });
1206
+ //# sourceMappingURL=products.js.map