shop-client 3.8.2 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +93 -1
  3. package/dist/checkout.mjs +1 -7
  4. package/dist/chunk-6GPWNCDO.mjs +130 -0
  5. package/dist/chunk-EJO5U4BT.mjs +2 -0
  6. package/dist/chunk-FFKWCNLU.mjs +1 -0
  7. package/dist/chunk-KYLPIEU3.mjs +2 -0
  8. package/dist/chunk-MB2INNNP.mjs +1 -0
  9. package/dist/chunk-MI7754VX.mjs +2 -0
  10. package/dist/chunk-SZQPMLZG.mjs +1 -0
  11. package/dist/collections.d.ts +1 -1
  12. package/dist/collections.mjs +1 -9
  13. package/dist/enrich-OZHBXKK6.mjs +1 -0
  14. package/dist/index.d.ts +24 -6
  15. package/dist/index.mjs +2 -702
  16. package/dist/products.d.ts +1 -1
  17. package/dist/products.mjs +1 -9
  18. package/dist/{store-CJVUz2Yb.d.mts → store-iQARl6J3.d.ts} +3 -3
  19. package/dist/store.d.ts +1 -1
  20. package/dist/store.mjs +1 -9
  21. package/dist/utils/rate-limit.d.ts +5 -0
  22. package/dist/utils/rate-limit.mjs +1 -11
  23. package/package.json +8 -10
  24. package/dist/checkout.d.mts +0 -31
  25. package/dist/checkout.js +0 -115
  26. package/dist/checkout.js.map +0 -1
  27. package/dist/checkout.mjs.map +0 -1
  28. package/dist/chunk-2KBOKOAD.mjs +0 -177
  29. package/dist/chunk-2KBOKOAD.mjs.map +0 -1
  30. package/dist/chunk-BWKBRM2Z.mjs +0 -136
  31. package/dist/chunk-BWKBRM2Z.mjs.map +0 -1
  32. package/dist/chunk-O4BPIIQ6.mjs +0 -503
  33. package/dist/chunk-O4BPIIQ6.mjs.map +0 -1
  34. package/dist/chunk-QCTICSBE.mjs +0 -398
  35. package/dist/chunk-QCTICSBE.mjs.map +0 -1
  36. package/dist/chunk-QL5OUZGP.mjs +0 -91
  37. package/dist/chunk-QL5OUZGP.mjs.map +0 -1
  38. package/dist/chunk-WTK5HUFI.mjs +0 -1287
  39. package/dist/chunk-WTK5HUFI.mjs.map +0 -1
  40. package/dist/collections.d.mts +0 -64
  41. package/dist/collections.js +0 -540
  42. package/dist/collections.js.map +0 -1
  43. package/dist/collections.mjs.map +0 -1
  44. package/dist/index.d.mts +0 -233
  45. package/dist/index.js +0 -3241
  46. package/dist/index.js.map +0 -1
  47. package/dist/index.mjs.map +0 -1
  48. package/dist/products.d.mts +0 -63
  49. package/dist/products.js +0 -1206
  50. package/dist/products.js.map +0 -1
  51. package/dist/products.mjs.map +0 -1
  52. package/dist/store-CJVUz2Yb.d.ts +0 -608
  53. package/dist/store.d.mts +0 -1
  54. package/dist/store.js +0 -698
  55. package/dist/store.js.map +0 -1
  56. package/dist/store.mjs.map +0 -1
  57. package/dist/utils/rate-limit.d.mts +0 -25
  58. package/dist/utils/rate-limit.js +0 -203
  59. package/dist/utils/rate-limit.js.map +0 -1
  60. package/dist/utils/rate-limit.mjs.map +0 -1
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 ShopSearch
3
+ Copyright (c) 2025 ShopClient
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -6,6 +6,18 @@
6
6
 
7
7
  `shop-client` is a powerful, type-safe TypeScript library for fetching and transforming product data from Shopify stores. Perfect for building e-commerce applications, product catalogs, price comparison tools, and automated store analysis.
8
8
 
9
+ ## Contents
10
+
11
+ - [Installation](#-installation)
12
+ - [Quick Start](#-quick-start)
13
+ - [Store Info Caching & Concurrency](#store-info-caching--concurrency)
14
+ - [Browser Usage](#browser-usage)
15
+ - [Server/Edge Usage](#serveredge-usage)
16
+ - [Deep Imports & Tree-Shaking](#deep-imports--tree-shaking)
17
+ - [Rate Limiting](#rate-limiting)
18
+ - [Migration: Barrel → Subpath Imports](#migration-barrel--subpath-imports)
19
+ - [API Docs](#api-docs)
20
+
9
21
  ## 🚀 Features
10
22
 
11
23
  - **Complete Store Data Access**: Fetch products, collections, and store information
@@ -17,6 +29,66 @@
17
29
  - **Zero Dependencies**: Lightweight with minimal external dependencies
18
30
  - **Store Type Classification**: Infers audience and verticals from showcased products (body_html-only)
19
31
 
32
+ ## 🧠 Store Info Caching & Concurrency
33
+
34
+ `getInfo()` uses time-based caching and in-flight request deduping to avoid redundant network calls:
35
+
36
+ - Cache window: `5 minutes` (`cacheExpiry`). Fresh cached results return immediately.
37
+ - You can configure this TTL via the `ShopClient` constructor option `cacheTTL` (milliseconds).
38
+ - Cached fields: `infoCacheValue` (last `StoreInfo`) and `infoCacheTimestamp` (last fetch time).
39
+ - In-flight deduping: concurrent calls share a single request via an internal promise; result is cached and returned to all callers.
40
+ - Failure handling: the in-flight marker clears in a `finally` block so subsequent calls can retry.
41
+
42
+ Behavior:
43
+ - Cached and fresh → returns cached `StoreInfo`.
44
+ - Stale/missing and request in-flight → awaits shared promise.
45
+ - Stale/missing and no in-flight → performs fetch, caches, returns.
46
+
47
+ Example: configure cache TTL
48
+
49
+ ```ts
50
+ import { ShopClient } from "shop-client";
51
+
52
+ // Set store info + validation cache TTL to 10 seconds
53
+ const shop = new ShopClient("https://exampleshop.com", { cacheTTL: 10_000 });
54
+ const info = await shop.getInfo();
55
+ ```
56
+
57
+ Manual invalidation
58
+
59
+ ```ts
60
+ import { ShopClient } from "shop-client";
61
+
62
+ const shop = new ShopClient("https://exampleshop.com", { cacheTTL: 60_000 });
63
+
64
+ // Fetch and cache
65
+ await shop.getInfo();
66
+
67
+ // Invalidate cache proactively (e.g., after a content update)
68
+ shop.clearInfoCache();
69
+
70
+ // Next call refetches and repopulates cache
71
+ await shop.getInfo();
72
+ ```
73
+
74
+ Force refetch
75
+
76
+ ```ts
77
+ import { ShopClient } from "shop-client";
78
+
79
+ const shop = new ShopClient("https://exampleshop.com", { cacheTTL: 60_000 });
80
+
81
+ // First call caches within TTL
82
+ await shop.getInfo();
83
+
84
+ // Force a fresh network fetch even if the cache is still fresh
85
+ const fresh = await shop.getInfo({ force: true });
86
+ ```
87
+
88
+ See also:
89
+ - Architecture: [Caching Strategy](./ARCHITECTURE.md#caching-strategy)
90
+ - API Reference (LLM): [ShopClientOptions, getInfo(force), clearInfoCache](./.llm/api-reference.md#constructor)
91
+
20
92
  ## 📦 Installation
21
93
 
22
94
  ```bash
@@ -133,6 +205,21 @@ Notes:
133
205
 
134
206
  ### Migration: Barrel → Subpath Imports
135
207
 
208
+ #### Package Rename: `shop-search` → `shop-client` (v3.8.2)
209
+ - Install: `npm i shop-client` (replaces `shop-search`)
210
+ - Update imports to `shop-client` (API unchanged)
211
+
212
+ TypeScript:
213
+ ```ts
214
+ // Before (pre-rename: shop-search)
215
+ import { Store } from 'shop-search';
216
+ const store = new Store("your-store.myshopify.com");
217
+
218
+ // After (post-rename: shop-client v3.8.2+)
219
+ import { ShopClient } from 'shop-client';
220
+ const client = new ShopClient("your-store.myshopify.com");
221
+ ```
222
+
136
223
  You can keep using the root entry (`shop-client`), but for smaller bundles switch to deep imports. The API remains the same—only the import paths change.
137
224
 
138
225
  Examples:
@@ -165,7 +252,12 @@ Notes:
165
252
  - No bundler changes required; deep imports are exposed via `exports`.
166
253
  - The root entry continues to work; prefer deep imports for production apps.
167
254
 
168
- ## ��️ Rate Limiting
255
+ ## API Docs
256
+
257
+ - TypeDoc builds automatically on pushes to `main` and publishes to GitHub Pages.
258
+ - Visit: `https://peppyhop.github.io/shop-client/` (after the first successful workflow run).
259
+
260
+ ## Rate Limiting
169
261
 
170
262
  `shop-client` ships with an opt-in, global rate limiter that transparently throttles all internal HTTP requests (products, collections, store info, enrichment). This helps avoid `429 Too Many Requests` responses and keeps crawling stable.
171
263
 
package/dist/checkout.mjs CHANGED
@@ -1,7 +1 @@
1
- import {
2
- createCheckoutOperations
3
- } from "./chunk-QL5OUZGP.mjs";
4
- export {
5
- createCheckoutOperations
6
- };
7
- //# sourceMappingURL=checkout.mjs.map
1
+ export{a as createCheckoutOperations}from'./chunk-SZQPMLZG.mjs';
@@ -0,0 +1,130 @@
1
+ import {b}from'./chunk-MB2INNNP.mjs';import M from'turndown';import {gfm}from'turndown-plugin-gfm';function C(t){let e=t||process.env.OPENROUTER_API_KEY;if(!e)throw new Error("Missing OpenRouter API key. Set OPENROUTER_API_KEY or pass apiKey.");return e}function T(t){if(t.startsWith("http://")||t.startsWith("https://"))try{let e=new URL(t);return `${e.protocol}//${e.hostname}`}catch{return t}return `https://${t}`}async function x(t,e){let r=`${T(t)}/products/${e}.js`,n=await b(r,{rateLimitClass:"products:ajax"});if(!n.ok)throw new Error(`Failed to fetch AJAX product: ${r}`);return await n.json()}async function D(t,e){let r=`${T(t)}/products/${e}`,n=await b(r,{rateLimitClass:"products:html"});if(!n.ok)throw new Error(`Failed to fetch product page: ${r}`);return n.text()}function K(t){let e=t.match(/<section[^>]*id="shopify-section-template--.*?__main"[^>]*>/);if(!e)return null;let o=t.indexOf(e[0]);if(o===-1)return null;let r=t.indexOf("</section>",o);return r===-1?null:t.substring(o,r+10)}function R(t,e){if(!t)return "";let o=new M({headingStyle:"atx",codeBlockStyle:"fenced",bulletListMarker:"-",emDelimiter:"*",strongDelimiter:"**",linkStyle:"inlined"});(e?.useGfm??true)&&o.use(gfm),["script","style","nav","footer"].forEach(s=>{o.remove(i=>i.nodeName?.toLowerCase()===s);});let n=s=>o.remove(i=>(typeof i.getAttribute=="function"&&i.getAttribute("class")||"").split(/\s+/).includes(s));return ["product-form","shopify-payment-button","shopify-payment-buttons","product__actions","product__media-wrapper","loox-rating","jdgm-widget","stamped-reviews"].forEach(n),["button","input","select","label"].forEach(s=>{o.remove(i=>i.nodeName?.toLowerCase()===s);}),["quantity-selector","product-atc-wrapper"].forEach(n),o.turndown(t)}async function F(t,e,o){let r=o?.inputType??"markdown",n=r==="html"?"BODY HTML":"BODY MARKDOWN",s=r==="html"?"PAGE HTML":"PAGE MARKDOWN",i=o?.outputFormat==="json"?`You are extracting structured buyer-useful information from Shopify product content.
2
+
3
+ Inputs:
4
+ 1) ${n}: ${r==="html"?"Raw Shopify product body_html":"Cleaned version of Shopify product body_html"}
5
+ 2) ${s}: ${r==="html"?"Raw product page HTML (main section)":"Extracted product page HTML converted to markdown"}
6
+
7
+ Return ONLY valid JSON (no markdown, no code fences) with this shape:
8
+ {
9
+ "title": null | string,
10
+ "description": null | string,
11
+ "materials": string[] | [],
12
+ "care": string[] | [],
13
+ "fit": null | string,
14
+ "images": null | string[],
15
+ "returnPolicy": null | string
16
+ }
17
+
18
+ Rules:
19
+ - Do not invent facts; if a field is unavailable, use null or []
20
+ - Prefer concise, factual statements
21
+ - Do NOT include product gallery/hero images in "images"; include only documentation images like size charts or measurement guides. If none, set "images": null.
22
+
23
+ ${n}:
24
+ ${t}
25
+
26
+ ${s}:
27
+ ${e}
28
+ `:`
29
+ You are enriching a Shopify product for a modern shopping-discovery app.
30
+
31
+ Inputs:
32
+ 1) ${n}: ${r==="html"?"Raw Shopify product body_html":"Cleaned version of Shopify product body_html"}
33
+ 2) ${s}: ${r==="html"?"Raw product page HTML (main section)":"Extracted product page HTML converted to markdown"}
34
+
35
+ Your tasks:
36
+ - Merge them into a single clean markdown document
37
+ - Remove duplicate content
38
+ - Remove product images
39
+ - Remove UI text, buttons, menus, review widgets, theme junk
40
+ - Remove product options
41
+ - Keep only available buyer-useful info: features, materials, care, fit, size chart, return policy, size chart, care instructions
42
+ - Include image of size-chart if present
43
+ - Don't include statements like information not available.
44
+ - Maintain structured headings (## Description, ## Materials, etc.)
45
+ - Output ONLY markdown (no commentary)
46
+
47
+ ${n}:
48
+ ${t}
49
+
50
+ ${s}:
51
+ ${e}
52
+ `,p=C(o?.apiKey),l=process.env.OPENROUTER_MODEL||"openai/gpt-4o-mini",u=o?.model??l,y=await $(u,i,p);if(o?.outputFormat==="json"){let f=y.replace(/```json|```/g,"").trim(),d=S(f);if(!d.ok)throw new Error(`LLM returned invalid JSON: ${d.error}`);let g=J(d.value);if(!g.ok)throw new Error(`LLM JSON schema invalid: ${g.error}`);let a=d.value;if(Array.isArray(a.images)){let b=a.images.filter(c=>{if(typeof c!="string")return false;let m=c.toLowerCase();return !["cdn.shopify.com","/products/","%2Fproducts%2F","_large","_grande","_1024x1024","_2048x"].some(h=>m.includes(h))});a.images=b.length>0?b:null;}return JSON.stringify(a)}return y}function S(t){try{return {ok:!0,value:JSON.parse(t)}}catch(e){return {ok:false,error:e?.message||"Failed to parse JSON"}}}function J(t){if(!t||typeof t!="object"||Array.isArray(t))return {ok:false,error:"Top-level must be a JSON object"};let e=t;if("title"in e&&!(e.title===null||typeof e.title=="string"))return {ok:false,error:"title must be null or string"};if("description"in e&&!(e.description===null||typeof e.description=="string"))return {ok:false,error:"description must be null or string"};if("fit"in e&&!(e.fit===null||typeof e.fit=="string"))return {ok:false,error:"fit must be null or string"};if("returnPolicy"in e&&!(e.returnPolicy===null||typeof e.returnPolicy=="string"))return {ok:false,error:"returnPolicy must be null or string"};let o=(r,n)=>{if(!Array.isArray(r))return {ok:false,error:`${n} must be an array`};for(let s of r)if(typeof s!="string")return {ok:false,error:`${n} items must be strings`};return {ok:true}};if("materials"in e){let r=o(e.materials,"materials");if(!r.ok)return r}if("care"in e){let r=o(e.care,"care");if(!r.ok)return r}if("images"in e){if(!(e.images===null||Array.isArray(e.images)))return {ok:false,error:"images must be null or an array"};if(Array.isArray(e.images)){let r=o(e.images,"images");if(!r.ok)return r}}return {ok:true}}async function $(t,e,o){if(process.env.OPENROUTER_OFFLINE==="1")return U(e);let r={"Content-Type":"application/json",Authorization:`Bearer ${o}`},n=process.env.OPENROUTER_SITE_URL||process.env.SITE_URL,s=process.env.OPENROUTER_APP_TITLE||"Shop Client";n&&(r["HTTP-Referer"]=n),(r["X-Title"]=s);let i=g=>({model:g,messages:[{role:"user",content:e}],temperature:.2}),l=[`${(process.env.OPENROUTER_BASE_URL||"https://openrouter.ai/api/v1").replace(/\/$/,"")}/chat/completions`],u=(process.env.OPENROUTER_FALLBACK_MODELS||"").split(",").map(g=>g.trim()).filter(Boolean),y=process.env.OPENROUTER_MODEL||"openai/gpt-4o-mini",f=Array.from(new Set([t,...u,y])).filter(Boolean),d="";for(let g of f)for(let a of l)try{let b$1=new AbortController,c=setTimeout(()=>b$1.abort(),15e3),m=await b(a,{method:"POST",headers:r,body:JSON.stringify(i(g)),signal:b$1.signal,rateLimitClass:"ai:openrouter"});if(clearTimeout(c),!m.ok){d=await m.text()||`${a}: HTTP ${m.status}`,await new Promise(P=>setTimeout(P,300));continue}let v=await m.json(),w=v?.choices?.[0]?.message?.content;if(typeof w=="string")return w;d=JSON.stringify(v),await new Promise(h=>setTimeout(h,200));}catch(b){d=`${a}: ${b?.message||String(b)}`,await new Promise(c=>setTimeout(c,200));}throw new Error(`OpenRouter request failed: ${d}`)}function U(t){let e=t.toLowerCase();return e.includes("return only valid json")&&e.includes('"audience":')?JSON.stringify({audience:"generic",vertical:"clothing",category:null,subCategory:null}):e.includes("return only valid json")&&e.includes('"materials":')?JSON.stringify({title:null,description:null,materials:[],care:[],fit:null,images:null,returnPolicy:null}):["## Description","Offline merge of product body and page.","","## Materials","- Not available"].join(`
53
+ `)}async function Y(t,e,o){let r=await x(t,e),n=r.description||"",s=await D(t,e),i=K(s),p=o?.inputType??"markdown",l=p==="html"?n:R(n,{useGfm:o?.useGfm}),u=p==="html"?i||s:R(i,{useGfm:o?.useGfm}),y=await F(l,u,{apiKey:o?.apiKey,inputType:p,model:o?.model,outputFormat:o?.outputFormat});if(o?.outputFormat==="json")try{let f=JSON.parse(y);if(f&&Array.isArray(f.images)){let d=[];if(r.featured_image&&d.push(String(r.featured_image)),Array.isArray(r.images))for(let c of r.images)typeof c=="string"&&c.length>0&&d.push(c);if(Array.isArray(r.media))for(let c of r.media)c?.src&&d.push(String(c.src));if(Array.isArray(r.variants))for(let c of r.variants){let m=c?.featured_image;m?.src&&d.push(String(m.src));}let g=new Set(d.map(c=>String(c).toLowerCase())),a=f.images.filter(c=>{if(typeof c!="string")return !1;let m=c.toLowerCase();return g.has(m)?!1:!["cdn.shopify.com","/products/","%2Fproducts%2F","_large","_grande","_1024x1024","_2048x"].some(h=>m.includes(h))});f.images=a.length>0?a:null;let b=JSON.stringify(f);return {bodyHtml:n,pageHtml:s,extractedMainHtml:i||"",mergedMarkdown:b}}}catch{}return {bodyHtml:n,pageHtml:s,extractedMainHtml:i||"",mergedMarkdown:y}}async function V(t,e){let o=C(e?.apiKey),r=process.env.OPENROUTER_MODEL||"openai/gpt-4o-mini",n=e?.model??r,s=`Classify the following product using a three-tiered hierarchy:
54
+
55
+ Product Content:
56
+ ${t}
57
+
58
+ Classification Rules:
59
+ 1. First determine the vertical (main product category)
60
+ 2. Then determine the category (specific type within that vertical)
61
+ 3. Finally determine the subCategory (sub-type within that category)
62
+
63
+ Vertical must be one of: clothing, beauty, accessories, home-decor, food-and-beverages
64
+ Audience must be one of: adult_male, adult_female, kid_male, kid_female, generic
65
+
66
+ Hierarchy Examples:
67
+ - Clothing \u2192 tops \u2192 t-shirts
68
+ - Clothing \u2192 footwear \u2192 sneakers
69
+ - Beauty \u2192 skincare \u2192 moisturizers
70
+ - Accessories \u2192 bags \u2192 backpacks
71
+ - Home-decor \u2192 furniture \u2192 chairs
72
+ - Food-and-beverages \u2192 snacks \u2192 chips
73
+
74
+ IMPORTANT CONSTRAINTS:
75
+ - Category must be relevant to the chosen vertical
76
+ - subCategory must be relevant to both vertical and category
77
+ - subCategory must be a single word or hyphenated words (no spaces)
78
+ - subCategory should NOT be material (e.g., "cotton", "leather") or color (e.g., "red", "blue")
79
+ - Focus on product type/function, not attributes
80
+
81
+ If you're not confident about category or sub-category, you can leave them optional.
82
+
83
+ Return ONLY valid JSON (no markdown, no code fences) with keys:
84
+ {
85
+ "audience": "adult_male" | "adult_female" | "kid_male" | "kid_female" | "generic",
86
+ "vertical": "clothing" | "beauty" | "accessories" | "home-decor" | "food-and-beverages",
87
+ "category": null | string,
88
+ "subCategory": null | string
89
+ }`,p=(await $(n,s,o)).replace(/```json|```/g,"").trim(),l=S(p);if(!l.ok)throw new Error(`LLM returned invalid JSON: ${l.error}`);let u=H(l.value);if(!u.ok)throw new Error(`LLM JSON schema invalid: ${u.error}`);return u.value}function H(t){if(!t||typeof t!="object"||Array.isArray(t))return {ok:false,error:"Top-level must be a JSON object"};let e=t,o=["adult_male","adult_female","kid_male","kid_female","generic"];if(typeof e.audience!="string"||!o.includes(e.audience))return {ok:false,error:"audience must be one of: adult_male, adult_female, kid_male, kid_female, generic"};let r=["clothing","beauty","accessories","home-decor","food-and-beverages"];if(typeof e.vertical!="string"||!r.includes(e.vertical))return {ok:false,error:"vertical must be one of: clothing, beauty, accessories, home-decor, food-and-beverages"};if("category"in e&&!(e.category===null||typeof e.category=="string"))return {ok:false,error:"category must be null or string"};if("subCategory"in e&&!(e.subCategory===null||typeof e.subCategory=="string"))return {ok:false,error:"subCategory must be null or string"};if(typeof e.subCategory=="string"){let n=e.subCategory.trim();if(!/^[A-Za-z0-9-]+$/.test(n))return {ok:false,error:"subCategory must be single word or hyphenated, no spaces"}}return {ok:true,value:{audience:e.audience,vertical:e.vertical,category:typeof e.category=="string"?e.category:e.category??null,subCategory:typeof e.subCategory=="string"?e.subCategory:e.subCategory??null}}}async function q(t,e){let o=C(e?.apiKey),r=process.env.OPENROUTER_MODEL||"openai/gpt-4o-mini",n=e?.model??r;if(process.env.OPENROUTER_OFFLINE==="1"){let y=Array.isArray(t.tags)?t.tags.slice(0,6):[],f=t.title.trim().slice(0,50),d=(t.vendor||"").trim(),g=typeof t.price=="number"?`$${t.price}`:"",a=d?`${f} | ${d}`:f,b=`Discover ${t.title}. ${g?`Priced at ${g}. `:""}Crafted to delight customers with quality and style.`.slice(0,160),c=`${t.title} \u2014 ${d||"Premium"} quality, designed to impress.`,m=t.description||`Introducing ${t.title}, combining performance and style for everyday use.`,v=`Get ${t.title} today${g?` for ${g}`:""}. Limited availability \u2014 don\u2019t miss out!`,w={metaTitle:a,metaDescription:b,shortDescription:c,longDescription:m,tags:y.length?y:["new","featured","popular"],marketingCopy:v},h=E(w);if(!h.ok)throw new Error(`Offline SEO content invalid: ${h.error}`);return h.value}let s=`Generate SEO-optimized content for this product:
90
+
91
+ Title: ${t.title}
92
+ Description: ${t.description||"N/A"}
93
+ Vendor: ${t.vendor||"N/A"}
94
+ Price: ${typeof t.price=="number"?`$${t.price}`:"N/A"}
95
+ Tags: ${Array.isArray(t.tags)&&t.tags.length?t.tags.join(", "):"N/A"}
96
+
97
+ Create compelling, SEO-friendly content that will help this product rank well and convert customers.
98
+
99
+ Return ONLY valid JSON (no markdown, no code fences) with keys: {
100
+ "metaTitle": string,
101
+ "metaDescription": string,
102
+ "shortDescription": string,
103
+ "longDescription": string,
104
+ "tags": string[],
105
+ "marketingCopy": string
106
+ }`,p=(await $(n,s,o)).replace(/```json|```/g,"").trim(),l=S(p);if(!l.ok)throw new Error(`LLM returned invalid JSON: ${l.error}`);let u=E(l.value);if(!u.ok)throw new Error(`LLM JSON schema invalid: ${u.error}`);return u.value}function E(t){if(!t||typeof t!="object"||Array.isArray(t))return {ok:false,error:"Top-level must be a JSON object"};let e=t,o=["metaTitle","metaDescription","shortDescription","longDescription","marketingCopy"];for(let r of o)if(typeof e[r]!="string"||!e[r].trim())return {ok:false,error:`${r} must be a non-empty string`};if(!Array.isArray(e.tags))return {ok:false,error:"tags must be an array"};for(let r of e.tags)if(typeof r!="string")return {ok:false,error:"tags items must be strings"};return {ok:true,value:{metaTitle:String(e.metaTitle),metaDescription:String(e.metaDescription),shortDescription:String(e.shortDescription),longDescription:String(e.longDescription),tags:e.tags,marketingCopy:String(e.marketingCopy)}}}async function W(t,e){let o=C(e?.apiKey),r=process.env.OPENROUTER_MODEL||"openai/gpt-4o-mini",n=e?.model??r,s=Array.isArray(t.showcase.products)?t.showcase.products.slice(0,10).map(a=>{if(typeof a=="string")return `- ${a}`;let b=typeof a?.productType=="string"&&a.productType.trim()?a.productType:"N/A";return `- ${String(a?.title||"N/A")}: ${b}`}):[],i=Array.isArray(t.showcase.collections)?t.showcase.collections.slice(0,5).map(a=>typeof a=="string"?`- ${a}`:`- ${String(a?.title||"N/A")}`):[],p=`Store Title: ${t.title}
107
+ Store Description: ${t.description??"N/A"}
108
+
109
+ Sample Products:
110
+ ${s.join(`
111
+ `)||"- N/A"}
112
+
113
+ Sample Collections:
114
+ ${i.join(`
115
+ `)||"- N/A"}`,l=`${t.title} ${t.description??""} ${s.join(" ")} ${i.join(" ")}`.toLowerCase();if(process.env.OPENROUTER_OFFLINE==="1"){let a=`${t.title} ${t.description??""} ${s.join(" ")} ${i.join(" ")}`.toLowerCase(),b={clothing:/(dress|shirt|pant|jean|hoodie|tee|t[- ]?shirt|sneaker|apparel|clothing)/,beauty:/(skincare|moisturizer|serum|beauty|cosmetic|makeup)/,accessories:/(bag|belt|watch|wallet|accessor(y|ies)|sunglasses|jewell?ery)/,"home-decor":/(sofa|chair|table|decor|home|candle|lamp|rug)/,"food-and-beverages":/(snack|food|beverage|coffee|tea|chocolate|gourmet)/},c={kid:/(\bkid\b|\bchild\b|\bchildren\b|\btoddler\b|\bboy\b|\bgirl\b)/,kid_male:/\bboys\b|\bboy\b/,kid_female:/\bgirls\b|\bgirl\b/,adult_male:/\bmen\b|\bmale\b|\bman\b|\bmens\b/,adult_female:/\bwomen\b|\bfemale\b|\bwoman\b|\bwomens\b/},m=[];c.kid?.test(a)?(c.kid_male?.test(a)&&m.push("kid_male"),c.kid_female?.test(a)&&m.push("kid_female"),!c.kid_male?.test(a)&&!c.kid_female?.test(a)&&m.push("generic")):(c.adult_male?.test(a)&&m.push("adult_male"),c.adult_female?.test(a)&&m.push("adult_female"),m.length===0&&m.push("generic"));let v=Object.entries(b).filter(([,k])=>k.test(a)).map(([k])=>k);v.length===0&&v.push("accessories");let w=s.join(" ").toLowerCase(),P=Object.entries({shirts:/(shirt|t[- ]?shirt|tee)/,pants:/(pant|trouser|chino)/,shorts:/shorts?/,jeans:/jeans?/,dresses:/dress/,skincare:/(serum|moisturizer|skincare|cream)/,accessories:/(belt|watch|wallet|bag)/,footwear:/(sneaker|shoe|boot)/,decor:/(candle|lamp|rug|sofa|chair|table)/,beverages:/(coffee|tea|chocolate)/}).filter(([,k])=>k.test(w)).map(([k])=>k),L=P.length?P:["general"],O={};for(let k of m){O[k]=O[k]||{};for(let N of v)O[k][N]=Array.from(new Set(L));}return A(O,l)}let u=`Analyze this store and build a multi-audience breakdown of verticals and categories.
116
+ Store Information:
117
+ ${p}
118
+
119
+ Return ONLY valid JSON (no markdown, no code fences) using this shape:
120
+ {
121
+ "adult_male": { "clothing": ["shirts", "pants"], "accessories": ["belts"] },
122
+ "adult_female": { "beauty": ["skincare"], "clothing": ["dresses"] },
123
+ "generic": { "clothing": ["t-shirts"] }
124
+ }
125
+
126
+ Rules:
127
+ - Keys MUST be audience: "adult_male" | "adult_female" | "kid_male" | "kid_female" | "generic".
128
+ - Nested keys MUST be vertical: "clothing" | "beauty" | "accessories" | "home-decor" | "food-and-beverages".
129
+ - Values MUST be non-empty arrays of category strings.
130
+ `,f=(await $(n,u,o)).replace(/```json|```/g,"").trim(),d=S(f);if(!d.ok)throw new Error(`LLM returned invalid JSON: ${d.error}`);let g=I(d.value);if(!g.ok)throw new Error(`LLM JSON schema invalid: ${g.error}`);return A(g.value,l)}function I(t){if(!t||typeof t!="object"||Array.isArray(t))return {ok:false,error:"Top-level must be an object keyed by audience"};let e=["adult_male","adult_female","kid_male","kid_female","generic"],o=["clothing","beauty","accessories","home-decor","food-and-beverages"],r=t,n={},s=Object.keys(r);if(s.length===0)return {ok:false,error:"At least one audience key is required"};for(let i of s){if(!e.includes(i))return {ok:false,error:`Invalid audience key: ${i}`};let p=r[i];if(!p||typeof p!="object"||Array.isArray(p))return {ok:false,error:`Audience ${i} must map to an object of verticals`};let l={};for(let u of Object.keys(p)){if(!o.includes(u))return {ok:false,error:`Invalid vertical key ${u} for audience ${i}`};let y=p[u];if(!Array.isArray(y)||y.length===0||!y.every(f=>typeof f=="string"&&f.trim()))return {ok:false,error:`Vertical ${u} for audience ${i} must be a non-empty array of strings`};l[u]=y.map(f=>f.trim());}n[i]=l;}return {ok:true,value:n}}function A(t,e){let o={kid:/(\bkid\b|\bchild\b|\bchildren\b|\btoddler\b|\bboy\b|\bgirl\b)/,kid_male:/\bboys\b|\bboy\b/,kid_female:/\bgirls\b|\bgirl\b/,adult_male:/\bmen\b|\bmale\b|\bman\b|\bmens\b/,adult_female:/\bwomen\b|\bfemale\b|\bwoman\b|\bwomens\b/},r={clothing:/(dress|shirt|pant|jean|hoodie|tee|t[- ]?shirt|sneaker|apparel|clothing)/,beauty:/(skincare|moisturizer|serum|beauty|cosmetic|makeup)/,accessories:/(bag|belt|watch|wallet|accessor(y|ies)|sunglasses|jewell?ery)/,"home-decor":/(sofa|chair|table|candle|lamp|rug|furniture|home[- ]?decor|homeware|housewares|living\s?room|dining\s?table|bed(?:room)?|wall\s?(art|mirror|clock))/,"food-and-beverages":/(snack|food|beverage|coffee|tea|chocolate|gourmet)/},n=new Set;o.kid?.test(e)?(o.kid_male?.test(e)&&n.add("kid_male"),o.kid_female?.test(e)&&n.add("kid_female"),!o.kid_male?.test(e)&&!o.kid_female?.test(e)&&n.add("generic")):(o.adult_male?.test(e)&&n.add("adult_male"),o.adult_female?.test(e)&&n.add("adult_female"),n.size===0&&n.add("generic"));let s=new Set(Object.entries(r).filter(([,l])=>l.test(e)).map(([l])=>l)||[]);s.size===0&&s.add("accessories");let i={};for(let[l,u]of Object.entries(t)){let y=l;if(!n.has(y))continue;let f={};for(let[d,g]of Object.entries(u||{})){let a=d;s.has(a)&&(f[a]=g);}Object.keys(f).length>0&&(i[y]=f);}if(Object.keys(i).length===0){let l={};for(let u of Array.from(s))l[u]=["general"];i.generic=l;}return (i.adult_male&&Object.keys(i.adult_male).length>0||i.adult_female&&Object.keys(i.adult_female).length>0)&&delete i.generic,i}export{x as a,D as b,K as c,R as d,F as e,Y as f,V as g,q as h,W as i,A as j};
@@ -0,0 +1,2 @@
1
+ import {i}from'./chunk-KYLPIEU3.mjs';import {b}from'./chunk-MB2INNNP.mjs';import {filter,isNonNullish}from'remeda';function k(m,l,R,K,A,M,b$1){let h=new Map,T=r=>{let t=h.get(r);if(t){if(Date.now()-t.ts<3e5)return t.value;h.delete(r);}},C=(r,t)=>{h.set(r,{ts:Date.now(),value:t});};function y(r,t){let i$1=r.priceMin??r.price??0,e=r.priceMax??r.price??0,n=r.compareAtPriceMin??r.compareAtPrice??0;return {...r,currency:t,localizedPricing:{currency:t,priceFormatted:i(i$1,t),priceMinFormatted:i(i$1,t),priceMaxFormatted:i(e,t),compareAtPriceFormatted:i(n,t)}}}function E(r,t){return !r||!t?r:r.map(i=>y(i,t))}let u={all:async r=>{let i=[];async function e(){let n=1;for(;;){let o=await R(n,250);if(!o||o.length===0||o.length<250){o&&o.length>0&&i.push(...o);break}i.push(...o),n++;}return i}try{let n=await e();return E(n,r?.currency)}catch(n){throw console.error("Failed to fetch all products:",l,n),n}},paginated:async r=>{let t=r?.page??1,i=Math.min(r?.limit??250,250),e=`${m}products.json?limit=${i}&page=${t}`;try{let n=await b(e,{rateLimitClass:"products:paginated"});if(!n.ok)throw console.error(`HTTP error! status: ${n.status} for ${l} page ${t}`),new Error(`HTTP error! status: ${n.status}`);let o=await n.json();if(o.products.length===0)return [];let a=K(o.products);return E(a,r?.currency)}catch(n){return console.error(`Error fetching products for ${l} page ${t} with limit ${i}:`,n),null}},find:async(r,t)=>{if(!r||typeof r!="string")throw new Error("Product handle is required and must be a string");try{let i=null;if(r.includes("?")){let d=r.split("?"),P=d[0]??r,O=d[1]??null;r=P,i=O;}let e=r.trim().replace(/[^a-zA-Z0-9\-_]/g,"");if(!e)throw new Error("Invalid product handle format");if(e.length>255)throw new Error("Product handle is too long");let n=T(e);if(typeof n<"u")return t?.currency?n?y(n,t.currency):null:n;let o=e;try{let d=await b(`${m}products/${encodeURIComponent(e)}`,{rateLimitClass:"products:resolve"});if(d.ok){let P=d.url;if(P){let S=new URL(P).pathname.replace(/\/$/,"").split("/").filter(Boolean),$=S.indexOf("products"),w=$>=0?S[$+1]:void 0;typeof w=="string"&&w.length&&(o=w);}}}catch{}let a=`${m}products/${encodeURIComponent(o)}.js${i?`?${i}`:""}`,s=await b(a,{rateLimitClass:"products:single"});if(!s.ok){if(s.status===404)return null;throw new Error(`HTTP error! status: ${s.status}`)}let c=await s.json(),f=A(c);return C(e,f),o!==e&&C(o,f),t?.currency?y(f,t.currency):f}catch(i){throw i instanceof Error&&console.error(`Error fetching product ${r}:`,m,i.message),i}},enriched:async(r,t)=>{if(!r||typeof r!="string")throw new Error("Product handle is required and must be a string");let i=t?.apiKey||process.env.OPENROUTER_API_KEY;if(!i)throw new Error("Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY.");let e=await u.find(r);if(!e)return null;let n=e.handle,{enrichProduct:o}=await import('./enrich-OZHBXKK6.mjs'),a=await o(l,n,{apiKey:i,useGfm:t?.useGfm,inputType:t?.inputType,model:t?.model,outputFormat:t?.outputFormat});return {...e,enriched_content:a.mergedMarkdown}},classify:async(r,t)=>{if(!r||typeof r!="string")throw new Error("Product handle is required and must be a string");let i=t?.apiKey||process.env.OPENROUTER_API_KEY;if(!i)throw new Error("Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY.");let e=await u.enriched(r,{apiKey:i,inputType:"html",model:t?.model,outputFormat:"json"});if(!e||!e.enriched_content)return null;let n=e.enriched_content;try{let s=JSON.parse(e.enriched_content),c=[];s.title&&typeof s.title=="string"&&c.push(`Title: ${s.title}`),s.description&&typeof s.description=="string"&&c.push(`Description: ${s.description}`),Array.isArray(s.materials)&&s.materials.length&&c.push(`Materials: ${s.materials.join(", ")}`),Array.isArray(s.care)&&s.care.length&&c.push(`Care: ${s.care.join(", ")}`),s.fit&&typeof s.fit=="string"&&c.push(`Fit: ${s.fit}`),s.returnPolicy&&typeof s.returnPolicy=="string"&&c.push(`ReturnPolicy: ${s.returnPolicy}`),n=c.join(`
2
+ `);}catch{}let{classifyProduct:o}=await import('./enrich-OZHBXKK6.mjs');return await o(n,{apiKey:i,model:t?.model})},generateSEOContent:async(r,t)=>{if(!r||typeof r!="string")throw new Error("Product handle is required and must be a string");let i=t?.apiKey||process.env.OPENROUTER_API_KEY;if(!i)throw new Error("Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY.");let e=await u.find(r);if(!e)return null;let n={title:e.title,description:e.bodyHtml||void 0,vendor:e.vendor,price:e.price,tags:e.tags},{generateSEOContent:o}=await import('./enrich-OZHBXKK6.mjs');return await o(n,{apiKey:i,model:t?.model})},showcased:async()=>{let r=await M(),t=await Promise.all(r.showcase.products.map(i=>b$1(i)));return filter(t,isNonNullish)},filter:async()=>{try{let r=await u.all();if(!r||r.length===0)return {};let t={};r.forEach(e=>{e.variants&&e.variants.length>0&&(e.options&&e.options.length>0&&e.options.forEach(n=>{let o=n.name.toLowerCase();t[o]||(t[o]=new Set),n.values.forEach(a=>{let s=a?.trim();if(s){let c=t[o];c||(c=new Set,t[o]=c),c.add(s.toLowerCase());}});}),e.variants.forEach(n=>{if(n.option1){let o=(e.options?.[0]?.name||"Option 1").toLowerCase(),a=t[o];a||(a=new Set,t[o]=a),a.add(n.option1.trim().toLowerCase());}if(n.option2){let o=(e.options?.[1]?.name||"Option 2").toLowerCase(),a=t[o];a||(a=new Set,t[o]=a),a.add(n.option2.trim().toLowerCase());}if(n.option3){let o=(e.options?.[2]?.name||"Option 3").toLowerCase();t[o]||(t[o]=new Set),t[o].add(n.option3.trim().toLowerCase());}}));});let i={};return Object.entries(t).forEach(([e,n])=>{i[e]=Array.from(n).sort();}),i}catch(r){throw console.error("Failed to create product filters:",l,r),r}}};return u}export{k as a};
@@ -0,0 +1 @@
1
+ import {i}from'./chunk-KYLPIEU3.mjs';import {b}from'./chunk-MB2INNNP.mjs';import {filter,isNonNullish}from'remeda';function $(u,p,h,y,m,P,b$1){let g=new Map,E=t=>{let e=g.get(t);if(e){if(Date.now()-e.ts<3e5)return e.value;g.delete(t);}},f=(t,e)=>{g.set(t,{ts:Date.now(),value:e});};function I(t,e){let r=t.priceMin??t.price??0,n=t.priceMax??t.price??0,o=t.compareAtPriceMin??t.compareAtPrice??0;return {...t,currency:e,localizedPricing:{currency:e,priceFormatted:i(r,e),priceMinFormatted:i(r,e),priceMaxFormatted:i(n,e),compareAtPriceFormatted:i(o,e)}}}function w(t,e){return !t||!e?t:t.map(r=>I(r,e))}return {paginated:async t=>{let e=t?.page??1,r=t?.limit??10;if(e<1||r<1||r>250)throw new Error("Invalid pagination parameters: page must be >= 1, limit must be between 1 and 250");try{return await h(e,r)??null}catch(n){return console.error("Failed to fetch paginated collections:",p,n),null}},all:async()=>{let e=[];async function r(){let n=1;for(;;){let o=await h(n,250);if(!o||o.length===0||o.length<250){if(!o){console.warn("fetchCollections returned null, treating as empty array.");break}o&&o.length>0&&e.push(...o);break}e.push(...o),n++;}return e}try{return await r()||[]}catch(n){throw console.error("Failed to fetch all collections:",p,n),n}},find:async t=>{if(!t||typeof t!="string")throw new Error("Collection handle is required and must be a string");let e=t.trim().replace(/[^a-zA-Z0-9\-_]/g,"");if(!e)throw new Error("Invalid collection handle format");if(e.length>255)throw new Error("Collection handle is too long");let r=E(e);if(typeof r<"u")return r;try{let n=`${u}collections/${encodeURIComponent(e)}.json`,o=await b(n,{rateLimitClass:"collections:single"});if(!o.ok){if(o.status===404)return null;throw new Error(`HTTP error! status: ${o.status}`)}let i=await o.json(),l=i.collection.image;if(!l){let d=(await m(i.collection.handle,{limit:1,page:1}))?.at(0),a=d?.images?.[0];d&&a&&(l={id:a.id,src:a.src,alt:a.alt||d.title,created_at:a.createdAt||new Date().toISOString()});}let c=y([{...i.collection,image:l}])[0]||null;return f(e,c),c?.handle&&c.handle!==e&&f(c.handle,c),c}catch(n){throw n instanceof Error&&console.error(`Error fetching collection ${e}:`,u,n.message),n}},showcased:async()=>{let t=await P(),e=await Promise.all(t.showcase.collections.map(r=>b$1(r)));return filter(e,isNonNullish)},products:{paginated:async(t,e)=>{if(!t||typeof t!="string")throw new Error("Collection handle is required and must be a string");let r=t.trim().replace(/[^a-zA-Z0-9\-_]/g,"");if(!r)throw new Error("Invalid collection handle format");if(r.length>255)throw new Error("Collection handle is too long");let n=e?.page??1,o=e?.limit??250;if(n<1||o<1||o>250)throw new Error("Invalid pagination parameters: page must be >= 1, limit must be between 1 and 250");let i=await m(r,{page:n,limit:o});return w(i,e?.currency)},all:async(t,e)=>{if(!t||typeof t!="string")throw new Error("Collection handle is required and must be a string");let r=t.trim().replace(/[^a-zA-Z0-9\-_]/g,"");if(!r)throw new Error("Invalid collection handle format");if(r.length>255)throw new Error("Collection handle is too long");try{let o=[],i=1;for(;;){let l=await m(r,{page:i,limit:250});if(!l||l.length===0||l.length<250){l&&l.length>0&&o.push(...l);break}o.push(...l),i++;}return w(o,e?.currency)}catch(n){return console.error(`Error fetching all products for collection ${r}:`,u,n),null}},slugs:async t=>{if(!t||typeof t!="string")throw new Error("Collection handle is required and must be a string");let e=t.trim().replace(/[^a-zA-Z0-9\-_]/g,"");if(!e)throw new Error("Invalid collection handle format");if(e.length>255)throw new Error("Collection handle is too long");try{let n=[],o=1;for(;;){let i=await m(e,{page:o,limit:250});if(!i||i.length===0||i.length<250){i&&i.length>0&&n.push(...i.map(l=>l.slug));break}n.push(...i.map(l=>l.slug)),o++;}return n}catch(r){return console.error(`Error fetching product slugs for collection ${e}:`,u,r),null}}}}}export{$ as a};
@@ -0,0 +1,2 @@
1
+ import {parse}from'tldts';function m(i){return parse(i).domainWithoutSuffix}function l(i){let e=new URL(i);return ((parse(e.href).domainWithoutSuffix??e.hostname.split(".")[0])||"").toLowerCase().replace(/[^a-z0-9]/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}var d=({handle:i,storeDomain:e})=>{let t=l(e);return `${i}-by-${t}`},g=(i,e)=>!e||e===0?0:Math.max(0,Math.round(100-i/e*100));function w(i,e){if(typeof i!="string")throw new Error("sanitizeDomain: input must be a string");let t=i.trim();if(!t)throw new Error("sanitizeDomain: input cannot be empty");!/^[a-z]+:\/\//i.test(t)&&!t.startsWith("//")&&(t=`https://${t}`);let r=e?.stripWWW??true;try{let n;t.startsWith("//")?n=new URL(`https:${t}`):t.includes("://")?n=new URL(t):n=new URL(`https://${t}`);let o=n.hostname.toLowerCase(),s=/^www\./i.test(n.hostname);if(r&&(o=o.replace(/^www\./,"")),!o.includes("."))throw new Error("sanitizeDomain: invalid domain (missing suffix)");let u=parse(o);if(!u.publicSuffix||u.isIcann===!1)throw new Error("sanitizeDomain: invalid domain (missing suffix)");return !r&&s?`www.${u.domain||o}`:u.domain||o}catch{let n=t.toLowerCase();n=n.replace(/^[a-z]+:\/\//,""),n=n.replace(/^\/\//,""),n=n.replace(/[/:#?].*$/,"");let o=/^www\./i.test(n);if(r&&(n=n.replace(/^www\./,"")),!n.includes("."))throw new Error("sanitizeDomain: invalid domain (missing suffix)");let s=parse(n);if(!s.publicSuffix||s.isIcann===false)throw new Error("sanitizeDomain: invalid domain (missing suffix)");return !r&&o?`www.${s.domain||n}`:s.domain||n}}function h(i){if(!i||typeof i!="string")return;let e=new Date(i);return Number.isNaN(e.getTime())?void 0:e}function f(i){return i.toLowerCase().replace(/\s+/g,"_")}function x(i,e){let t=i.map(f),a={};for(let r of e){let n=[];if(t[0]&&r.option1&&n.push(`${t[0]}#${f(r.option1)}`),t[1]&&r.option2&&n.push(`${t[1]}#${f(r.option2)}`),t[2]&&r.option3&&n.push(`${t[2]}#${f(r.option3)}`),n.length>0){n.length>1&&n.sort();let o=n.join("##"),s=r.id.toString();a[o]===void 0&&(a[o]=s);}}return a}function W(i,e){try{return new Intl.NumberFormat(void 0,{style:"currency",currency:e}).format((i||0)/100)}catch{return `${(i||0)/100} ${e}`}}
2
+ export{m as a,l as b,d as c,g as d,w as e,h as f,f as g,x as h,W as i};
@@ -0,0 +1 @@
1
+ var u=class{constructor(e){this.queue=[];this.inFlight=0;this.refillTimer=null;this.options=e,this.tokens=e.maxRequestsPerInterval;}startRefill(){this.refillTimer||(this.refillTimer=setInterval(()=>{this.tokens=this.options.maxRequestsPerInterval,this.tryRun();},this.options.intervalMs),this.refillTimer&&typeof this.refillTimer.unref=="function"&&this.refillTimer.unref());}ensureRefillStarted(){this.refillTimer||this.startRefill();}configure(e){this.options={...this.options,...e},this.options.maxRequestsPerInterval=Math.max(1,this.options.maxRequestsPerInterval),this.options.intervalMs=Math.max(10,this.options.intervalMs),this.options.maxConcurrency=Math.max(1,this.options.maxConcurrency);}schedule(e){return new Promise((s,n)=>{this.ensureRefillStarted(),this.queue.push({fn:e,resolve:s,reject:n}),this.tryRun();})}tryRun(){for(;this.queue.length>0&&this.inFlight<this.options.maxConcurrency&&this.tokens>0;){let e=this.queue.shift();this.tokens-=1,this.inFlight+=1,Promise.resolve().then(e.fn).then(s=>e.resolve(s)).catch(s=>e.reject(s)).finally(()=>{this.inFlight-=1,setTimeout(()=>this.tryRun(),0);});}}},h=false,l={maxRequestsPerInterval:5,intervalMs:1e3,maxConcurrency:5},R=new u(l),m=new Map,f=new Map;function b(t){try{if(typeof t=="string")return new URL(t).host;if(t instanceof URL)return t.host;let e=t.url;if(e)return new URL(e).host}catch{}}function L(t){if(!t)return;let e=m.get(t);if(e)return e;for(let[s,n]of m.entries())if(s.startsWith("*.")&&t.endsWith(s.slice(2)))return n}function M(t){typeof t.enabled=="boolean"&&(h=t.enabled);let{perHost:e,perClass:s}=t,n={};if(typeof t.maxRequestsPerInterval=="number"&&(n.maxRequestsPerInterval=t.maxRequestsPerInterval),typeof t.intervalMs=="number"&&(n.intervalMs=t.intervalMs),typeof t.maxConcurrency=="number"&&(n.maxConcurrency=t.maxConcurrency),Object.keys(n).length&&R.configure(n),e)for(let i of Object.keys(e)){let r=e[i],a=m.get(i);a?a.configure(r):m.set(i,new u({maxRequestsPerInterval:r.maxRequestsPerInterval??l.maxRequestsPerInterval,intervalMs:r.intervalMs??l.intervalMs,maxConcurrency:r.maxConcurrency??l.maxConcurrency}));}if(s)for(let i of Object.keys(s)){let r=s[i],a=f.get(i);a?a.configure(r):f.set(i,new u({maxRequestsPerInterval:r.maxRequestsPerInterval??l.maxRequestsPerInterval,intervalMs:r.intervalMs??l.intervalMs,maxConcurrency:r.maxConcurrency??l.maxConcurrency}));}}function P(t){return new Promise(e=>setTimeout(e,t))}async function q(t,e){let s=e?.rateLimitClass,n=s?f.get(s):void 0,i=L(b(t)),r=h?n??i??R:void 0,a=Math.max(0,e?.retry?.maxRetries??2),p=Math.max(0,e?.retry?.baseDelayMs??200),v=e?.retry?.retryOnStatuses??[429,503],c=0,y=null,o=null;for(;c<=a;){try{if(r?o=await r.schedule(()=>fetch(t,e)):o=await fetch(t,e),!o||o.ok||!v.includes(o.status))return o}catch(g){y=g;}if(c+=1,c>a)break;let x=Math.floor(Math.random()*100),d=p*2**(c-1)+x;await P(d);}if(o)return o;throw y??new Error("rateLimitedFetch failed without response")}function I(){return {enabled:h,options:{...l}}}export{M as a,q as b,I as c};
@@ -0,0 +1,2 @@
1
+ import {a,b as b$1,e}from'./chunk-KYLPIEU3.mjs';import {b}from'./chunk-MB2INNNP.mjs';import {unique}from'remeda';var O={"+1":"US","+44":"GB","+61":"AU","+65":"SG","+91":"IN","+81":"JP","+49":"DE","+33":"FR","+971":"AE","+39":"IT","+34":"ES","+82":"KR","+55":"BR","+62":"ID","+92":"PK","+7":"RU"},N={Rs:"IN","\u20B9":"IN",$:"US",CA$:"CA",A$:"AU","\xA3":"GB","\u20AC":"EU",AED:"AE","\u20A9":"KR","\xA5":"JP"},K={Rs:"INR","\u20B9":"INR",$:"USD",CA$:"CAD",A$:"AUD","\xA3":"GBP","\u20AC":"EUR",AED:"AED","\u20A9":"KRW","\xA5":"JPY"},T={INR:"IN",USD:"US",CAD:"CA",AUD:"AU",GBP:"GB",EUR:"EU",AED:"AE",KRW:"KR",JPY:"JP"};function d(s,o,f,y){o&&(s[o]||(s[o]={score:0,reasons:[]}),s[o].score+=f,s[o].reasons.push(y));}async function M(s){let o={},f,y=s.match(/<script[^>]+id=["']shopify-features["'][^>]*>([\s\S]*?)<\/script>/);if(y)try{let n=y[1];if(n){let e=JSON.parse(n);if(e.country&&d(o,e.country,1,"shopify-features.country"),e.locale?.includes("-")){let[,r]=e.locale.split("-");r&&d(o,r.toUpperCase(),.7,"shopify-features.locale");}if(e.moneyFormat){for(let r in N)if(e.moneyFormat.includes(r)){let l=N[r];typeof l=="string"&&d(o,l,.6,"moneyFormat symbol");let a=K[r];!f&&typeof a=="string"&&(f=a);}}}}catch{}let P=s.match(/Shopify\.currency\s*=\s*(\{[^}]*\})/);if(P)try{let n=P[1],e=JSON.parse(n||"{}"),r=typeof e?.active=="string"?e.active.toUpperCase():void 0,l=r?T[r]:void 0;r&&(f=r),typeof l=="string"&&d(o,l,.8,"Shopify.currency.active");}catch{}else {let n=s.match(/Shopify\.currency\.active\s*=\s*['"]([A-Za-z]{3})['"]/i);if(n){let e=n[1],r=typeof e=="string"?e.toUpperCase():void 0,l=r?T[r]:void 0;r&&(f=r),typeof l=="string"&&d(o,l,.8,"Shopify.currency.active");}}let U=s.match(/Shopify\.country\s*=\s*['"]([A-Za-z]{2})['"]/i);if(U){let n=U[1],e=typeof n=="string"?n.toUpperCase():void 0;typeof e=="string"&&d(o,e,1,"Shopify.country");}let C=s.match(/\+\d{1,3}[\s\-()0-9]{5,}/g);if(C)for(let n of C){let e=n.match(/^\+\d{1,3}/)?.[0];e&&O[e]&&d(o,O[e],.8,`phone prefix ${e}`);}let c=/<script[^>]+application\/ld\+json[^>]*>(.*?)<\/script>/g,p=c.exec(s);for(;p!==null;){try{let n=p[1];if(n){let e=JSON.parse(n),r=(a,g=[])=>{if(Array.isArray(a)){for(let E of a)r(E,g);return g}if(a&&typeof a=="object"){let E=a,m=E.address;if(m&&typeof m=="object"){let D=m.addressCountry;typeof D=="string"&&g.push(D);}let b=E["@graph"];b&&r(b,g);}return g},l=r(e);for(let a of l)d(o,a,1,"JSON-LD addressCountry");}}catch{}p=c.exec(s);}let S=s.match(/<footer[^>]*>(.*?)<\/footer>/i);if(S){let n=S[1],e=n?n.toLowerCase():"",r={india:"IN","united states":"US",canada:"CA",australia:"AU","united kingdom":"GB",britain:"GB",uk:"GB",japan:"JP","south korea":"KR",korea:"KR",germany:"DE",france:"FR",italy:"IT",spain:"ES",brazil:"BR",russia:"RU",singapore:"SG",indonesia:"ID",pakistan:"PK"};for(let[l,a]of Object.entries(r))e.includes(l)&&d(o,a,.4,"footer mention");}let R=Object.entries(o).sort((n,e)=>e[1].score-n[1].score)[0];return R?{country:R[0],confidence:Math.min(1,R[1].score/2),signals:R[1].reasons,currencyCode:f}:{country:"Unknown",confidence:0,signals:[],currencyCode:f}}async function j(s){let{baseUrl:o,storeDomain:f,validateProductExists:y,validateCollectionExists:P,validateLinksInBatches:U}=s,C=await b(o,{rateLimitClass:"store:info"});if(!C.ok)throw new Error(`HTTP error! status: ${C.status}`);let c=await C.text(),p=t=>{let u=new RegExp(`<meta[^>]*name=["']${t}["'][^>]*content=["'](.*?)["']`),i=c.match(u);return i?i[1]:null},S=t=>{let u=new RegExp(`<meta[^>]*property=["']${t}["'][^>]*content=["'](.*?)["']`),i=c.match(u);return i?i[1]:null},v=p("og:site_name")??a(o),R=p("og:title")??p("twitter:title"),n=p("description")||S("og:description"),e$1=p("shopify-digital-wallet")?.split("/")[1],r=c.match(/['"](.*?\.myshopify\.com)['"]/),l=r?r[1]:null,a$1=S("og:image")||S("og:image:secure_url");if(a$1)a$1=a$1.replace("http://","https://");else {let u=c.match(/<img[^>]+src=["']([^"']+\/cdn\/shop\/[^"']+)["']/)?.[1];a$1=u?u.replace("http://","https://"):null;}let g={},E=/<a[^>]+href=["']([^"']*(?:facebook|twitter|instagram|pinterest|youtube|linkedin|tiktok|vimeo)\.com[^"']*)["']/g;for(let t of c.matchAll(E)){let u=t[1];if(!u)continue;let i=u;try{i.startsWith("//")?i=`https:${i}`:i.startsWith("/")&&(i=new URL(i,o).toString());let h=new URL(i),A=h.hostname.replace("www.","").split(".")[0];A&&(g[A]=h.toString());}catch{}}let m={tel:null,email:null,contactPage:null};for(let t of c.matchAll(/href=["']tel:([^"']+)["']/g))m.tel=t?.[1]?.trim()||null;for(let t of c.matchAll(/href=["']mailto:([^"']+)["']/g))m.email=t?.[1]?.trim()||null;for(let t of c.matchAll(/href=["']([^"']*(?:\/contact|\/pages\/contact)[^"']*)["']/g))m.contactPage=t?.[1]||null;let b$2=c.match(/href=["']([^"']*\/products\/[^"']+)["']/g)?.map(t=>t?.split("href=")[1]?.replace(/["']/g,"")?.split("/").at(-1))?.filter(Boolean)||[],D=c.match(/href=["']([^"']*\/collections\/[^"']+)["']/g)?.map(t=>t?.split("href=")[1]?.replace(/["']/g,"")?.split("/").at(-1))?.filter(Boolean)||[],J=c.match(/<(header|nav|div|section)\b[^>]*\b(?:id|class)=["'][^"']*(?=.*shopify-section)(?=.*\b(header|navigation|nav|menu)\b)[^"']*["'][^>]*>[\s\S]*?<\/\1>/gi)?.flatMap(t=>t.match(/href=["']([^"']+)["']/g)?.filter(i=>i.includes("/products/")||i.includes("/collections/")||i.includes("/pages/"))?.map(i=>{let h=i.match(/href=["']([^"']+)["']/)?.[1];if(h&&!h.startsWith("#")&&!h.startsWith("javascript:"))try{return new URL(h,f).pathname.replace(/^\/|\/$/g,"")}catch{return h.replace(/^\/|\/$/g,"")}return null}).filter(i=>!!i)??[])??[],k=b$1(o),w=await M(c),[F,Y]=await Promise.all([U(b$2.filter(t=>!!t),t=>y(t)),U(D.filter(t=>!!t),t=>P(t))]),$={name:v||k,domain:e(o),slug:k,title:R||null,description:n||null,logoUrl:a$1,socialLinks:g,contactLinks:m,headerLinks:J,showcase:{products:unique(F??[]),collections:unique(Y??[])},jsonLdData:c.match(/<script[^>]*type="application\/ld\+json"[^>]*>([^<]+)<\/script>/g)?.map(t=>t?.split(">")[1]?.replace(/<\/script/g,"")||null)?.map(t=>t?JSON.parse(t):null)||[],techProvider:{name:"shopify",walletId:e$1,subDomain:l??null},country:w.country},G=w?.currencyCode;return {info:$,currencyCode:G}}function X(s){return {info:async()=>{try{let{info:o}=await j({baseUrl:s.baseUrl,storeDomain:s.storeDomain,validateProductExists:s.validateProductExists,validateCollectionExists:s.validateCollectionExists,validateLinksInBatches:s.validateLinksInBatches});return o}catch(o){s.handleFetchError(o,"fetching store info",s.baseUrl);}}}}
2
+ export{M as a,j as b,X as c};
@@ -0,0 +1 @@
1
+ function p(s){return {createUrl:({email:n,items:i,address:t})=>{if(!n||!n.includes("@"))throw new Error("Invalid email address");if(!i||i.length===0)throw new Error("Items array cannot be empty");for(let r of i){if(!r.productVariantId||!r.quantity)throw new Error("Each item must have productVariantId and quantity");let e=Number.parseInt(r.quantity,10);if(Number.isNaN(e)||e<=0)throw new Error("Quantity must be a positive number")}let a=["firstName","lastName","address1","city","zip","country"];for(let r of a)if(!t[r])throw new Error(`Address field '${r}' is required`);let o=i.map(r=>`${encodeURIComponent(r.productVariantId)}:${encodeURIComponent(r.quantity)}`).join(","),c=new URLSearchParams({"checkout[email]":n,"checkout[shipping_address][first_name]":t.firstName,"checkout[shipping_address][last_name]":t.lastName,"checkout[shipping_address][address1]":t.address1,"checkout[shipping_address][city]":t.city,"checkout[shipping_address][zip]":t.zip,"checkout[shipping_address][country]":t.country,"checkout[shipping_address][province]":t.province,"checkout[shipping_address][phone]":t.phone});return `${s}cart/${o}?${c.toString()}`}}}export{p as a};
@@ -1,4 +1,4 @@
1
- import { f as Collection, y as CurrencyCode, c as Product, e as ShopifyCollection, g as StoreInfo } from './store-CJVUz2Yb.js';
1
+ import { f as Collection, y as CurrencyCode, c as Product, e as ShopifyCollection, g as StoreInfo } from './store-iQARl6J3.js';
2
2
 
3
3
  /**
4
4
  * Interface for collection operations
@@ -1,9 +1 @@
1
- import {
2
- createCollectionOperations
3
- } from "./chunk-QCTICSBE.mjs";
4
- import "./chunk-BWKBRM2Z.mjs";
5
- import "./chunk-2KBOKOAD.mjs";
6
- export {
7
- createCollectionOperations
8
- };
9
- //# sourceMappingURL=collections.mjs.map
1
+ export{a as createCollectionOperations}from'./chunk-FFKWCNLU.mjs';import'./chunk-KYLPIEU3.mjs';import'./chunk-MB2INNNP.mjs';
@@ -0,0 +1 @@
1
+ export{g as classifyProduct,i as determineStoreType,f as enrichProduct,c as extractMainSection,a as fetchAjaxProduct,b as fetchProductPage,h as generateSEOContent,d as htmlToMarkdown,e as mergeWithLLM,j as pruneBreakdownForSignals}from'./chunk-6GPWNCDO.mjs';import'./chunk-MB2INNNP.mjs';
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { CheckoutOperations } from './checkout.js';
2
2
  import { CollectionOperations } from './collections.js';
3
3
  import { ProductOperations } from './products.js';
4
- import { P as ProductClassification, S as SEOContent, C as CountryDetectionResult, a as StoreOperations, b as ShopifyProduct, c as Product, d as ShopifySingleProduct, e as ShopifyCollection, f as Collection, g as StoreInfo, h as StoreTypeBreakdown } from './store-CJVUz2Yb.js';
5
- export { H as Address, F as CatalogCategory, I as ContactUrls, K as CountryScore, N as CountryScores, J as Coupon, y as CurrencyCode, G as Demographics, Q as JsonLdEntry, L as LocalizedPricing, M as MetaTag, D as ProductImage, z as ProductOption, x as ProductPricing, B as ProductVariant, A as ProductVariantImage, E as ShopifyApiProduct, u as ShopifyBaseProduct, r as ShopifyBaseVariant, k as ShopifyBasicInfo, o as ShopifyFeaturedMedia, O as ShopifyFeaturesData, m as ShopifyImage, l as ShopifyImageDimensions, p as ShopifyMedia, q as ShopifyOption, w as ShopifyPredictiveProductSearch, v as ShopifyProductAndStore, s as ShopifyProductVariant, t as ShopifySingleProductVariant, j as ShopifyTimestamps, n as ShopifyVariantImage, i as StoreCatalog, R as StoreTypeResult, V as ValidStoreCatalog } from './store-CJVUz2Yb.js';
4
+ import { P as ProductClassification, S as SEOContent, C as CountryDetectionResult, a as StoreOperations, b as ShopifyProduct, c as Product, d as ShopifySingleProduct, e as ShopifyCollection, f as Collection, g as StoreInfo, h as StoreTypeBreakdown } from './store-iQARl6J3.js';
5
+ export { H as Address, F as CatalogCategory, I as ContactUrls, K as CountryScore, N as CountryScores, J as Coupon, y as CurrencyCode, G as Demographics, Q as JsonLdEntry, L as LocalizedPricing, M as MetaTag, D as ProductImage, z as ProductOption, x as ProductPricing, B as ProductVariant, A as ProductVariantImage, E as ShopifyApiProduct, u as ShopifyBaseProduct, r as ShopifyBaseVariant, k as ShopifyBasicInfo, o as ShopifyFeaturedMedia, O as ShopifyFeaturesData, m as ShopifyImage, l as ShopifyImageDimensions, p as ShopifyMedia, q as ShopifyOption, w as ShopifyPredictiveProductSearch, v as ShopifyProductAndStore, s as ShopifyProductVariant, t as ShopifySingleProductVariant, j as ShopifyTimestamps, n as ShopifyVariantImage, i as StoreCatalog, R as StoreTypeResult, V as ValidStoreCatalog } from './store-iQARl6J3.js';
6
6
  export { configureRateLimit } from './utils/rate-limit.js';
7
7
 
8
8
  /**
@@ -91,7 +91,7 @@ declare function safeParseDate(input?: string | null): Date | undefined;
91
91
  *
92
92
  * @example
93
93
  * ```typescript
94
- * import { ShopClient } from 'shop-search';
94
+ * import { ShopClient } from 'shop-client';
95
95
  *
96
96
  * const shop = new ShopClient('https://exampleshop.com');
97
97
  *
@@ -102,6 +102,9 @@ declare function safeParseDate(input?: string | null): Date | undefined;
102
102
  * const storeInfo = await shop.getInfo();
103
103
  * ```
104
104
  */
105
+ type ShopClientOptions = {
106
+ cacheTTL?: number;
107
+ };
105
108
  declare class ShopClient {
106
109
  private storeDomain;
107
110
  private baseUrl;
@@ -111,6 +114,9 @@ declare class ShopClient {
111
114
  private cacheTimestamps;
112
115
  private normalizeImageUrlCache;
113
116
  private storeCurrency?;
117
+ private infoCacheValue?;
118
+ private infoCacheTimestamp?;
119
+ private infoInFlight?;
114
120
  products: ProductOperations;
115
121
  collections: CollectionOperations;
116
122
  checkout: CheckoutOperations;
@@ -135,7 +141,7 @@ declare class ShopClient {
135
141
  * const shop2 = new ShopClient('https://boutique.fashion');
136
142
  * ```
137
143
  */
138
- constructor(urlPath: string);
144
+ constructor(urlPath: string, options?: ShopClientOptions);
139
145
  /**
140
146
  * Optimized image URL normalization with caching
141
147
  */
@@ -217,7 +223,19 @@ declare class ShopClient {
217
223
  * console.log(storeInfo.country); // "US"
218
224
  * ```
219
225
  */
220
- getInfo(): Promise<StoreInfo>;
226
+ /**
227
+ * Optionally bypass cache and force a fresh fetch.
228
+ *
229
+ * @param options - `{ force?: boolean }` when `true`, ignores cached value and TTL.
230
+ */
231
+ getInfo(options?: {
232
+ force?: boolean;
233
+ }): Promise<StoreInfo>;
234
+ /**
235
+ * Manually clear the cached store info.
236
+ * The next call to `getInfo()` will fetch fresh data regardless of TTL.
237
+ */
238
+ clearInfoCache(): void;
221
239
  /**
222
240
  * Determine the store's primary vertical and target audience.
223
241
  * Uses `getInfo()` internally; no input required.
@@ -230,4 +248,4 @@ declare class ShopClient {
230
248
  }): Promise<StoreTypeBreakdown>;
231
249
  }
232
250
 
233
- export { CheckoutOperations, Collection, CollectionOperations, CountryDetectionResult, Product, ProductClassification, ProductOperations, SEOContent, ShopClient, ShopifyCollection, ShopifyProduct, ShopifySingleProduct, StoreInfo, StoreOperations, StoreTypeBreakdown, calculateDiscount, classifyProduct, detectShopifyCountry, extractDomainWithoutSuffix, genProductSlug, generateSEOContent, generateStoreSlug, safeParseDate, sanitizeDomain };
251
+ export { CheckoutOperations, Collection, CollectionOperations, CountryDetectionResult, Product, ProductClassification, ProductOperations, SEOContent, ShopClient, type ShopClientOptions, ShopifyCollection, ShopifyProduct, ShopifySingleProduct, StoreInfo, StoreOperations, StoreTypeBreakdown, calculateDiscount, classifyProduct, detectShopifyCountry, extractDomainWithoutSuffix, genProductSlug, generateSEOContent, generateStoreSlug, safeParseDate, sanitizeDomain };