shop-client 3.10.0 → 3.12.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.
@@ -12,8 +12,8 @@ import {
12
12
 
13
13
  // src/client/get-info.ts
14
14
  import { unique } from "remeda";
15
- async function getInfoForStore(args) {
16
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
15
+ async function getInfoForStore(args, options) {
16
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
17
17
  const {
18
18
  baseUrl,
19
19
  storeDomain,
@@ -22,7 +22,8 @@ async function getInfoForStore(args) {
22
22
  validateLinksInBatches
23
23
  } = args;
24
24
  const response = await rateLimitedFetch(baseUrl, {
25
- rateLimitClass: "store:info"
25
+ rateLimitClass: "store:info",
26
+ timeoutMs: 7e3
26
27
  });
27
28
  if (!response.ok) {
28
29
  throw new Error(`HTTP error! status: ${response.status}`);
@@ -94,18 +95,22 @@ async function getInfoForStore(args) {
94
95
  )) {
95
96
  contactLinks.contactPage = (match == null ? void 0 : match[1]) || null;
96
97
  }
97
- const extractedProductLinks = ((_g = (_f = html.match(/href=["']([^"']*\/products\/[^"']+)["']/g)) == null ? void 0 : _f.map(
98
- (match) => {
99
- var _a2, _b2;
100
- return (_b2 = (_a2 = match == null ? void 0 : match.split("href=")[1]) == null ? void 0 : _a2.replace(/["']/g, "")) == null ? void 0 : _b2.split("/").at(-1);
101
- }
102
- )) == null ? void 0 : _g.filter(Boolean)) || [];
103
- const extractedCollectionLinks = ((_i = (_h = html.match(/href=["']([^"']*\/collections\/[^"']+)["']/g)) == null ? void 0 : _h.map(
104
- (match) => {
105
- var _a2, _b2;
106
- return (_b2 = (_a2 = match == null ? void 0 : match.split("href=")[1]) == null ? void 0 : _a2.replace(/["']/g, "")) == null ? void 0 : _b2.split("/").at(-1);
107
- }
108
- )) == null ? void 0 : _i.filter(Boolean)) || [];
98
+ const extractedProductLinks = unique(
99
+ ((_g = (_f = html.match(/href=["']([^"']*\/products\/[^"']+)["']/g)) == null ? void 0 : _f.map(
100
+ (match) => {
101
+ var _a2, _b2;
102
+ return (_b2 = (_a2 = match == null ? void 0 : match.split("href=")[1]) == null ? void 0 : _a2.replace(/["']/g, "")) == null ? void 0 : _b2.split("/").at(-1);
103
+ }
104
+ )) == null ? void 0 : _g.filter(Boolean)) || []
105
+ ).slice(0, 8);
106
+ const extractedCollectionLinks = unique(
107
+ ((_i = (_h = html.match(/href=["']([^"']*\/collections\/[^"']+)["']/g)) == null ? void 0 : _h.map(
108
+ (match) => {
109
+ var _a2, _b2;
110
+ return (_b2 = (_a2 = match == null ? void 0 : match.split("href=")[1]) == null ? void 0 : _a2.replace(/["']/g, "")) == null ? void 0 : _b2.split("/").at(-1);
111
+ }
112
+ )) == null ? void 0 : _i.filter(Boolean)) || []
113
+ ).slice(0, 8);
109
114
  const headerLinks = (_k = (_j = html.match(
110
115
  /<(header|nav|div|section)\b[^>]*\b(?:id|class)=["'][^"']*(?=.*shopify-section)(?=.*\b(header|navigation|nav|menu)\b)[^"']*["'][^>]*>[\s\S]*?<\/\1>/gi
111
116
  )) == null ? void 0 : _j.flatMap((header) => {
@@ -129,20 +134,37 @@ async function getInfoForStore(args) {
129
134
  })) != null ? _k : [];
130
135
  const slug = generateStoreSlug(baseUrl);
131
136
  const countryDetection = await detectShopCountry(html);
132
- const [homePageProductLinks, homePageCollectionLinks] = await Promise.all([
133
- validateLinksInBatches(
134
- extractedProductLinks.filter(
135
- (handle) => Boolean(handle)
137
+ const doValidate = (options == null ? void 0 : options.validateShowcase) === true;
138
+ let homePageProductLinks = [];
139
+ let homePageCollectionLinks = [];
140
+ if (doValidate) {
141
+ const batchSize = (_l = options == null ? void 0 : options.validationBatchSize) != null ? _l : 5;
142
+ const validated = await Promise.all([
143
+ validateLinksInBatches(
144
+ extractedProductLinks.filter(
145
+ (handle) => Boolean(handle)
146
+ ),
147
+ (handle) => validateProductExists(handle),
148
+ batchSize
136
149
  ),
137
- (handle) => validateProductExists(handle)
138
- ),
139
- validateLinksInBatches(
140
- extractedCollectionLinks.filter(
141
- (handle) => Boolean(handle)
142
- ),
143
- (handle) => validateCollectionExists(handle)
144
- )
145
- ]);
150
+ validateLinksInBatches(
151
+ extractedCollectionLinks.filter(
152
+ (handle) => Boolean(handle)
153
+ ),
154
+ (handle) => validateCollectionExists(handle),
155
+ batchSize
156
+ )
157
+ ]);
158
+ homePageProductLinks = (_m = validated[0]) != null ? _m : [];
159
+ homePageCollectionLinks = (_n = validated[1]) != null ? _n : [];
160
+ } else {
161
+ homePageProductLinks = extractedProductLinks.filter(
162
+ (handle) => Boolean(handle)
163
+ );
164
+ homePageCollectionLinks = extractedCollectionLinks.filter(
165
+ (handle) => Boolean(handle)
166
+ );
167
+ }
146
168
  const info = {
147
169
  name: name || slug,
148
170
  domain: sanitizeDomain(baseUrl),
@@ -157,20 +179,21 @@ async function getInfoForStore(args) {
157
179
  products: unique(homePageProductLinks != null ? homePageProductLinks : []),
158
180
  collections: unique(homePageCollectionLinks != null ? homePageCollectionLinks : [])
159
181
  },
160
- jsonLdData: ((_m = (_l = html.match(
182
+ jsonLdData: ((_p = (_o = html.match(
161
183
  /<script[^>]*type="application\/ld\+json"[^>]*>([^<]+)<\/script>/g
162
- )) == null ? void 0 : _l.map(
184
+ )) == null ? void 0 : _o.map(
163
185
  (match) => {
164
186
  var _a2;
165
187
  return ((_a2 = match == null ? void 0 : match.split(">")[1]) == null ? void 0 : _a2.replace(/<\/script/g, "")) || null;
166
188
  }
167
- )) == null ? void 0 : _m.map((json) => json ? JSON.parse(json) : null)) || [],
189
+ )) == null ? void 0 : _p.map((json) => json ? JSON.parse(json) : null)) || [],
168
190
  techProvider: {
169
191
  name: "shopify",
170
192
  walletId: shopifyWalletId,
171
193
  subDomain: myShopifySubdomain != null ? myShopifySubdomain : null
172
194
  },
173
- country: countryDetection.country
195
+ country: countryDetection.country,
196
+ currency: (countryDetection == null ? void 0 : countryDetection.currencyCode) || null
174
197
  };
175
198
  const currencyCode = countryDetection == null ? void 0 : countryDetection.currencyCode;
176
199
  return { info, currencyCode };
@@ -450,6 +450,74 @@ function createProductOperations(baseUrl, storeDomain, fetchProducts, productsDt
450
450
  console.error("Failed to create product filters:", storeDomain, error);
451
451
  throw error;
452
452
  }
453
+ },
454
+ predictiveSearch: async (query, options) => {
455
+ var _a, _b, _c, _d, _e;
456
+ if (!query || typeof query !== "string") {
457
+ throw new Error("Query is required and must be a string");
458
+ }
459
+ const limit = Math.max(1, Math.min((_a = options == null ? void 0 : options.limit) != null ? _a : 10, 10));
460
+ const unavailable = (options == null ? void 0 : options.unavailableProducts) === "show" || (options == null ? void 0 : options.unavailableProducts) === "hide" ? options.unavailableProducts : "hide";
461
+ const localeValue = (options == null ? void 0 : options.locale) && options.locale.trim() || "en";
462
+ const localePrefix = `${localeValue.replace(/^\/|\/$/g, "")}/`;
463
+ const url = `${baseUrl}${localePrefix}search/suggest.json?q=${encodeURIComponent(query)}&resources[type]=product&resources[limit]=${limit}&resources[options][unavailable_products]=${unavailable}`;
464
+ const response = await rateLimitedFetch(url, {
465
+ rateLimitClass: "search:predictive",
466
+ timeoutMs: 7e3,
467
+ retry: { maxRetries: 2, baseDelayMs: 300 }
468
+ });
469
+ let resp = response;
470
+ if (!resp.ok && (resp.status === 404 || resp.status === 417)) {
471
+ const fallbackUrl = `${baseUrl}search/suggest.json?q=${encodeURIComponent(query)}&resources[type]=product&resources[limit]=${limit}&resources[options][unavailable_products]=${unavailable}`;
472
+ resp = await rateLimitedFetch(fallbackUrl, {
473
+ rateLimitClass: "search:predictive",
474
+ timeoutMs: 7e3,
475
+ retry: { maxRetries: 2, baseDelayMs: 300 }
476
+ });
477
+ }
478
+ if (!resp.ok) {
479
+ throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
480
+ }
481
+ const data = await resp.json();
482
+ const raw = (_d = (_c = (_b = data == null ? void 0 : data.resources) == null ? void 0 : _b.results) == null ? void 0 : _c.products) != null ? _d : [];
483
+ const handles = raw.filter((p) => {
484
+ var _a2;
485
+ return Boolean((_a2 = p == null ? void 0 : p.available) != null ? _a2 : true);
486
+ }).map((p) => {
487
+ var _a2;
488
+ return String((_a2 = p == null ? void 0 : p.handle) != null ? _a2 : "");
489
+ }).filter((h) => h.length > 0).slice(0, limit);
490
+ const fetched = await Promise.all(handles.map((h) => findProduct(h)));
491
+ const results = filter(fetched, isNonNullish);
492
+ const finalProducts = (_e = maybeOverrideProductsCurrency(results, options == null ? void 0 : options.currency)) != null ? _e : [];
493
+ return finalProducts;
494
+ },
495
+ recommendations: async (productId, options) => {
496
+ var _a, _b;
497
+ if (!Number.isFinite(productId) || productId <= 0) {
498
+ throw new Error("Valid productId is required");
499
+ }
500
+ const limit = Math.max(1, Math.min((_a = options == null ? void 0 : options.limit) != null ? _a : 10, 10));
501
+ const intent = (options == null ? void 0 : options.intent) === "complementary" ? "complementary" : "related";
502
+ const localeValue = (options == null ? void 0 : options.locale) && options.locale.trim() || "en";
503
+ const localePrefix = `${localeValue.replace(/^\/|\/$/g, "")}/`;
504
+ const url = `${baseUrl}${localePrefix}recommendations/products.json?product_id=${encodeURIComponent(String(productId))}&limit=${limit}&intent=${intent}`;
505
+ const resp = await rateLimitedFetch(url, {
506
+ rateLimitClass: "products:recommendations",
507
+ timeoutMs: 7e3,
508
+ retry: { maxRetries: 2, baseDelayMs: 300 }
509
+ });
510
+ if (!resp.ok) {
511
+ if (resp.status === 404) {
512
+ return [];
513
+ }
514
+ throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
515
+ }
516
+ const data = await resp.json();
517
+ const productsArray = Array.isArray(data) ? data : Array.isArray(data == null ? void 0 : data.products) ? data.products : [];
518
+ const normalized = productsDto(productsArray) || [];
519
+ const finalProducts = (_b = maybeOverrideProductsCurrency(normalized, options == null ? void 0 : options.currency)) != null ? _b : [];
520
+ return finalProducts;
453
521
  }
454
522
  };
455
523
  return operations;
package/dist/index.d.ts CHANGED
@@ -132,6 +132,7 @@ declare class ShopClient {
132
132
  * - `jsonLdData` - Structured data from JSON-LD scripts
133
133
  * - `techProvider` - Shopify-specific information (walletId, subDomain)
134
134
  * - `country` - Country detection results with ISO 3166-1 alpha-2 codes (e.g., "US", "GB")
135
+ * - `currency` - ISO 4217 currency code inferred from store (e.g., "USD")
135
136
  *
136
137
  * @throws {Error} When the store URL is unreachable or returns an error
137
138
  *
@@ -153,6 +154,8 @@ declare class ShopClient {
153
154
  */
154
155
  getInfo(options?: {
155
156
  force?: boolean;
157
+ validateShowcase?: boolean;
158
+ validationBatchSize?: number;
156
159
  }): Promise<StoreInfo>;
157
160
  /**
158
161
  * Manually clear the cached store info.
package/dist/index.mjs CHANGED
@@ -6,11 +6,11 @@ import {
6
6
  } from "./chunk-554O5ED6.mjs";
7
7
  import {
8
8
  createProductOperations
9
- } from "./chunk-CNJRHWIK.mjs";
9
+ } from "./chunk-ZF4M6GMB.mjs";
10
10
  import {
11
11
  createStoreOperations,
12
12
  getInfoForStore
13
- } from "./chunk-MSIVKUGN.mjs";
13
+ } from "./chunk-VK5666EK.mjs";
14
14
  import {
15
15
  classifyProduct,
16
16
  determineStoreType,
@@ -568,7 +568,8 @@ var ShopClient = class {
568
568
  const url = `${this.baseUrl}products/${handle}.js`;
569
569
  const response = await rateLimitedFetch(url, {
570
570
  method: "HEAD",
571
- rateLimitClass: "validate:product"
571
+ rateLimitClass: "validate:product",
572
+ timeoutMs: 5e3
572
573
  });
573
574
  const exists = response.ok;
574
575
  this.setCacheValue(cacheKey, exists);
@@ -590,7 +591,8 @@ var ShopClient = class {
590
591
  const url = `${this.baseUrl}collections/${handle}.json`;
591
592
  const response = await rateLimitedFetch(url, {
592
593
  method: "HEAD",
593
- rateLimitClass: "validate:collection"
594
+ rateLimitClass: "validate:collection",
595
+ timeoutMs: 5e3
594
596
  });
595
597
  const exists = response.ok;
596
598
  this.setCacheValue(cacheKey, exists);
@@ -654,6 +656,7 @@ var ShopClient = class {
654
656
  * - `jsonLdData` - Structured data from JSON-LD scripts
655
657
  * - `techProvider` - Shopify-specific information (walletId, subDomain)
656
658
  * - `country` - Country detection results with ISO 3166-1 alpha-2 codes (e.g., "US", "GB")
659
+ * - `currency` - ISO 4217 currency code inferred from store (e.g., "USD")
657
660
  *
658
661
  * @throws {Error} When the store URL is unreachable or returns an error
659
662
  *
@@ -685,13 +688,19 @@ var ShopClient = class {
685
688
  return await this.infoInFlight;
686
689
  }
687
690
  this.infoInFlight = (async () => {
688
- const { info, currencyCode } = await getInfoForStore({
689
- baseUrl: this.baseUrl,
690
- storeDomain: this.storeDomain,
691
- validateProductExists: (handle) => this.validateProductExists(handle),
692
- validateCollectionExists: (handle) => this.validateCollectionExists(handle),
693
- validateLinksInBatches: (items, validator, batchSize) => this.validateLinksInBatches(items, validator, batchSize)
694
- });
691
+ const { info, currencyCode } = await getInfoForStore(
692
+ {
693
+ baseUrl: this.baseUrl,
694
+ storeDomain: this.storeDomain,
695
+ validateProductExists: (handle) => this.validateProductExists(handle),
696
+ validateCollectionExists: (handle) => this.validateCollectionExists(handle),
697
+ validateLinksInBatches: (items, validator, batchSize) => this.validateLinksInBatches(items, validator, batchSize)
698
+ },
699
+ {
700
+ validateShowcase: (options == null ? void 0 : options.validateShowcase) === true,
701
+ validationBatchSize: options == null ? void 0 : options.validationBatchSize
702
+ }
703
+ );
695
704
  if (typeof currencyCode === "string") {
696
705
  this.storeCurrency = currencyCode;
697
706
  }
@@ -55,6 +55,24 @@ interface ProductOperations {
55
55
  * Creates a filter map of variant options and their distinct values from all products.
56
56
  */
57
57
  filter(): Promise<Record<string, string[]> | null>;
58
+ /**
59
+ * Predictive product search using Shopify Ajax API.
60
+ */
61
+ predictiveSearch(query: string, options?: {
62
+ limit?: number;
63
+ locale?: string;
64
+ currency?: CurrencyCode;
65
+ unavailableProducts?: "show" | "hide" | "last";
66
+ }): Promise<Product[]>;
67
+ /**
68
+ * Product recommendations for a given product ID using Shopify Ajax API.
69
+ */
70
+ recommendations(productId: number, options?: {
71
+ limit?: number;
72
+ intent?: "related" | "complementary";
73
+ locale?: string;
74
+ currency?: CurrencyCode;
75
+ }): Promise<Product[] | null>;
58
76
  }
59
77
  /**
60
78
  * Creates product operations for a store instance
package/dist/products.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createProductOperations
3
- } from "./chunk-CNJRHWIK.mjs";
3
+ } from "./chunk-ZF4M6GMB.mjs";
4
4
  import "./chunk-D5MTUWFO.mjs";
5
5
  import "./chunk-U3RQRBXZ.mjs";
6
6
  export {
package/dist/store.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { J as JsonLdEntry, d as CountryDetectionResult } from './types-luPg5O08.js';
1
+ import { J as JsonLdEntry, d as CountryDetectionResult, f as CurrencyCode } from './types-luPg5O08.js';
2
2
 
3
3
  /**
4
4
  * Store operations interface for managing store-related functionality.
@@ -36,6 +36,7 @@ interface StoreInfo {
36
36
  subDomain: string | null;
37
37
  };
38
38
  country: CountryDetectionResult["country"];
39
+ currency: CurrencyCode | null;
39
40
  }
40
41
  /**
41
42
  * Creates store operations for a ShopClient instance.
package/dist/store.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createStoreOperations
3
- } from "./chunk-MSIVKUGN.mjs";
3
+ } from "./chunk-VK5666EK.mjs";
4
4
  import "./chunk-D5MTUWFO.mjs";
5
5
  import "./chunk-G7OCMGA6.mjs";
6
6
  import "./chunk-U3RQRBXZ.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shop-client",
3
- "version": "3.10.0",
3
+ "version": "3.12.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.mjs",
6
6
  "module": "./dist/index.mjs",