shop-client 3.13.0 → 3.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,8 +31,9 @@
31
31
 
32
32
  ## 🧠 Store Info Caching & Concurrency
33
33
 
34
- `getInfo()` uses time-based caching and in-flight request deduping to avoid redundant network calls:
34
+ `getInfo()` validates the store URL and uses time-based caching and in-flight request deduping to avoid redundant network calls:
35
35
 
36
+ - Validation: Ensures the URL points to a valid Shopify store (checks for specific meta tags and scripts). Throws an error if invalid.
36
37
  - Cache window: `5 minutes` (`cacheExpiry`). Fresh cached results return immediately.
37
38
  - You can configure this TTL via the `ShopClient` constructor option `cacheTTL` (milliseconds).
38
39
  - Cached fields: `infoCacheValue` (last `StoreInfo`) and `infoCacheTimestamp` (last fetch time).
@@ -12,7 +12,7 @@ import {
12
12
 
13
13
  // src/client/get-info.ts
14
14
  import { unique } from "remeda";
15
- async function getInfoForStore(args, options) {
15
+ async function getInfoForShop(args, options) {
16
16
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
17
17
  const {
18
18
  baseUrl,
@@ -36,6 +36,13 @@ async function getInfoForStore(args, options) {
36
36
  const match = html.match(regex);
37
37
  return match ? match[1] : null;
38
38
  };
39
+ const shopifyWalletId = (_a = getMetaTag("shopify-digital-wallet")) == null ? void 0 : _a.split("/")[1];
40
+ const isShopifyStore = html.includes("cdn.shopify.com") || html.includes("myshopify.com") || html.includes("shopify-digital-wallet") || html.includes("Shopify.shop") || html.includes("Shopify.currency") || html.includes("shopify-section");
41
+ if (!isShopifyStore || !shopifyWalletId) {
42
+ throw new Error(
43
+ "The provided URL does not appear to be a valid Shopify store."
44
+ );
45
+ }
39
46
  const getPropertyMetaTag = (property) => {
40
47
  const regex = new RegExp(
41
48
  `<meta[^>]*property=["']${property}["'][^>]*content=["'](.*?)["']`
@@ -43,10 +50,9 @@ async function getInfoForStore(args, options) {
43
50
  const match = html.match(regex);
44
51
  return match ? match[1] : null;
45
52
  };
46
- const name = (_a = getMetaTag("og:site_name")) != null ? _a : extractDomainWithoutSuffix(baseUrl);
47
- const title = (_b = getMetaTag("og:title")) != null ? _b : getMetaTag("twitter:title");
53
+ const name = (_b = getMetaTag("og:site_name")) != null ? _b : extractDomainWithoutSuffix(baseUrl);
54
+ const title = (_c = getMetaTag("og:title")) != null ? _c : getMetaTag("twitter:title");
48
55
  const description = getMetaTag("description") || getPropertyMetaTag("og:description");
49
- const shopifyWalletId = (_c = getMetaTag("shopify-digital-wallet")) == null ? void 0 : _c.split("/")[1];
50
56
  const myShopifySubdomainMatch = html.match(/['"](.*?\.myshopify\.com)['"]/);
51
57
  const myShopifySubdomain = myShopifySubdomainMatch ? myShopifySubdomainMatch[1] : null;
52
58
  let logoUrl = getPropertyMetaTag("og:image") || getPropertyMetaTag("og:image:secure_url");
@@ -188,7 +194,7 @@ async function getInfoForStore(args, options) {
188
194
  }
189
195
  )) == null ? void 0 : _p.map((json) => json ? JSON.parse(json) : null)) || [],
190
196
  techProvider: {
191
- name: "shopify",
197
+ name: shopifyWalletId ? "shopify" : "",
192
198
  walletId: shopifyWalletId,
193
199
  subDomain: myShopifySubdomain != null ? myShopifySubdomain : null
194
200
  },
@@ -200,7 +206,7 @@ async function getInfoForStore(args, options) {
200
206
  }
201
207
 
202
208
  // src/store.ts
203
- function createStoreOperations(context) {
209
+ function createShopOperations(context) {
204
210
  return {
205
211
  /**
206
212
  * Fetches comprehensive store information including metadata, social links, and showcase content.
@@ -236,7 +242,7 @@ function createStoreOperations(context) {
236
242
  */
237
243
  info: async () => {
238
244
  try {
239
- const { info } = await getInfoForStore({
245
+ const { info } = await getInfoForShop({
240
246
  baseUrl: context.baseUrl,
241
247
  storeDomain: context.storeDomain,
242
248
  validateProductExists: context.validateProductExists,
@@ -247,11 +253,134 @@ function createStoreOperations(context) {
247
253
  } catch (error) {
248
254
  context.handleFetchError(error, "fetching store info", context.baseUrl);
249
255
  }
256
+ },
257
+ getMetaData: async () => {
258
+ try {
259
+ const response = await rateLimitedFetch(context.baseUrl, {
260
+ rateLimitClass: "store:metadata",
261
+ timeoutMs: 7e3
262
+ });
263
+ if (!response.ok) {
264
+ throw new Error(`HTTP error! status: ${response.status}`);
265
+ }
266
+ const html = await response.text();
267
+ const getPropertyMetaTag = (property) => {
268
+ const regex = new RegExp(
269
+ `<meta[^>]*property=["']${property}["'][^>]*content=["'](.*?)["']`
270
+ );
271
+ const match = html.match(regex);
272
+ return match ? match[1] : null;
273
+ };
274
+ const siteName = getPropertyMetaTag("og:site_name");
275
+ const title = getPropertyMetaTag("og:title");
276
+ const description = getPropertyMetaTag("og:description");
277
+ const url = getPropertyMetaTag("og:url");
278
+ const type = getPropertyMetaTag("og:type");
279
+ const image = getPropertyMetaTag("og:image");
280
+ const imageSecureUrl = getPropertyMetaTag("og:image:secure_url");
281
+ return {
282
+ siteName: siteName != null ? siteName : null,
283
+ title: title != null ? title : null,
284
+ description: description != null ? description : null,
285
+ url: url != null ? url : null,
286
+ type: type != null ? type : null,
287
+ image: image != null ? image : null,
288
+ imageSecureUrl: imageSecureUrl != null ? imageSecureUrl : null
289
+ };
290
+ } catch (error) {
291
+ context.handleFetchError(
292
+ error,
293
+ "fetching store metadata",
294
+ context.baseUrl
295
+ );
296
+ }
297
+ },
298
+ getJsonLd: async () => {
299
+ var _a, _b;
300
+ try {
301
+ const response = await rateLimitedFetch(context.baseUrl, {
302
+ rateLimitClass: "store:jsonld",
303
+ timeoutMs: 7e3
304
+ });
305
+ if (!response.ok) {
306
+ throw new Error(`HTTP error! status: ${response.status}`);
307
+ }
308
+ const html = await response.text();
309
+ const scripts = (_b = (_a = html.match(
310
+ /<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/g
311
+ )) == null ? void 0 : _a.map(
312
+ (match) => match.replace(/^.*?>/, "").replace(/<\/script>$/i, "")
313
+ )) != null ? _b : [];
314
+ const parsed = scripts.map((json) => {
315
+ try {
316
+ return JSON.parse(json);
317
+ } catch {
318
+ return void 0;
319
+ }
320
+ }).filter((x) => !!x);
321
+ return parsed.length ? parsed : void 0;
322
+ } catch (error) {
323
+ context.handleFetchError(
324
+ error,
325
+ "fetching store JSON-LD",
326
+ context.baseUrl
327
+ );
328
+ }
329
+ },
330
+ getHeaderLinks: async () => {
331
+ var _a;
332
+ try {
333
+ const response = await rateLimitedFetch(context.baseUrl, {
334
+ rateLimitClass: "store:header",
335
+ timeoutMs: 7e3
336
+ });
337
+ if (!response.ok) {
338
+ throw new Error(`HTTP error! status: ${response.status}`);
339
+ }
340
+ const html = await response.text();
341
+ const sections = (_a = html.match(
342
+ /<(header|nav|div|section)\b[^>]*\b(?:id|class)=["'][^"']*(?=.*shopify-section)(?=.*\b(header|navigation|nav|menu)\b)[^"']*["'][^>]*>[\s\S]*?<\/\1>/gi
343
+ )) != null ? _a : [];
344
+ const links = sections.flatMap((section) => {
345
+ var _a2;
346
+ return (_a2 = section.match(/href=["']([^"']+)["']/g)) != null ? _a2 : [];
347
+ }).map((link) => {
348
+ var _a2;
349
+ return (_a2 = link.match(/href=["']([^"']+)["']/)) == null ? void 0 : _a2[1];
350
+ }).filter((href) => !!href).filter(
351
+ (href) => href.includes("/products/") || href.includes("/collections/") || href.includes("/pages/")
352
+ ).map((href) => {
353
+ try {
354
+ if (href.startsWith("//"))
355
+ return new URL(`https:${href}`).pathname;
356
+ if (href.startsWith("/"))
357
+ return new URL(href, context.storeDomain).pathname;
358
+ return new URL(href).pathname;
359
+ } catch {
360
+ return href;
361
+ }
362
+ }).map((p) => p.replace(/^\/|\/$/g, ""));
363
+ const seen = /* @__PURE__ */ new Set();
364
+ const deduped = [];
365
+ for (const l of links) {
366
+ if (!seen.has(l)) {
367
+ seen.add(l);
368
+ deduped.push(l);
369
+ }
370
+ }
371
+ return deduped;
372
+ } catch (error) {
373
+ context.handleFetchError(
374
+ error,
375
+ "fetching header links",
376
+ context.baseUrl
377
+ );
378
+ }
250
379
  }
251
380
  };
252
381
  }
253
382
 
254
383
  export {
255
- getInfoForStore,
256
- createStoreOperations
384
+ getInfoForShop,
385
+ createShopOperations
257
386
  };
@@ -1,4 +1,4 @@
1
- import { StoreInfo } from './store.js';
1
+ import { ShopInfo } from './store.js';
2
2
  import { C as Collection, f as CurrencyCode, P as Product, b as ShopifyCollection } from './types-luPg5O08.js';
3
3
 
4
4
  /**
@@ -60,6 +60,6 @@ interface CollectionOperations {
60
60
  declare function createCollectionOperations(baseUrl: string, storeDomain: string, fetchCollections: (page: number, limit: number) => Promise<Collection[] | null>, collectionsDto: (collections: ShopifyCollection[]) => Collection[], fetchPaginatedProductsFromCollection: (collectionHandle: string, options?: {
61
61
  page?: number;
62
62
  limit?: number;
63
- }) => Promise<Product[] | null>, getStoreInfo: () => Promise<StoreInfo>, findCollection: (handle: string) => Promise<Collection | null>): CollectionOperations;
63
+ }) => Promise<Product[] | null>, getStoreInfo: () => Promise<ShopInfo>, findCollection: (handle: string) => Promise<Collection | null>): CollectionOperations;
64
64
 
65
65
  export { type CollectionOperations, createCollectionOperations };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
+ import { S as ShopifyProduct, P as Product, a as ShopifySingleProduct, b as ShopifyCollection, C as Collection, J as JsonLdEntry, c as StoreTypeBreakdown } from './types-luPg5O08.js';
2
+ export { d as CountryDetectionResult, e as CountryScores, f as CurrencyCode, L as LocalizedPricing, M as MetaTag, g as ProductImage, h as ProductOption, i as ProductVariant, j as ProductVariantImage } from './types-luPg5O08.js';
1
3
  import { CheckoutOperations } from './checkout.js';
2
4
  import { CollectionOperations } from './collections.js';
3
5
  import { ProductOperations } from './products.js';
4
- import { StoreOperations, StoreInfo } from './store.js';
5
- import { S as ShopifyProduct, P as Product, a as ShopifySingleProduct, b as ShopifyCollection, C as Collection, c as StoreTypeBreakdown } from './types-luPg5O08.js';
6
- export { d as CountryDetectionResult, e as CountryScores, f as CurrencyCode, L as LocalizedPricing, M as MetaTag, g as ProductImage, h as ProductOption, i as ProductVariant, j as ProductVariantImage } from './types-luPg5O08.js';
6
+ import { ShopOperations, ShopInfo, OpenGraphMeta } from './store.js';
7
7
  export { classifyProduct, generateSEOContent } from './ai/enrich.js';
8
8
  export { detectShopCountry } from './utils/detect-country.js';
9
9
  export { calculateDiscount, extractDomainWithoutSuffix, genProductSlug, generateStoreSlug, safeParseDate, sanitizeDomain } from './utils/func.js';
@@ -40,10 +40,13 @@ declare class ShopClient {
40
40
  private infoCacheValue?;
41
41
  private infoCacheTimestamp?;
42
42
  private infoInFlight?;
43
+ private metaCacheValue?;
44
+ private metaCacheTimestamp?;
45
+ private metaInFlight?;
43
46
  products: ProductOperations;
44
47
  collections: CollectionOperations;
45
48
  checkout: CheckoutOperations;
46
- storeOperations: StoreOperations;
49
+ shopOperations: ShopOperations;
47
50
  /**
48
51
  * Creates a new ShopClient instance for interacting with a Shopify store.
49
52
  *
@@ -156,12 +159,21 @@ declare class ShopClient {
156
159
  force?: boolean;
157
160
  validateShowcase?: boolean;
158
161
  validationBatchSize?: number;
159
- }): Promise<StoreInfo>;
162
+ }): Promise<ShopInfo>;
160
163
  /**
161
164
  * Manually clear the cached store info.
162
165
  * The next call to `getInfo()` will fetch fresh data regardless of TTL.
163
166
  */
164
167
  clearInfoCache(): void;
168
+ /**
169
+ * Fetch OpenGraph metadata from the store homepage.
170
+ * Returns only `og:*` fields without additional parsing or validation.
171
+ */
172
+ getMetaData(options?: {
173
+ force?: boolean;
174
+ }): Promise<OpenGraphMeta>;
175
+ getJsonLd(): Promise<JsonLdEntry[] | undefined>;
176
+ getHeaderLinks(): Promise<string[]>;
165
177
  /**
166
178
  * Determine the store's primary vertical and target audience.
167
179
  * Uses `getInfo()` internally; no input required.
@@ -174,4 +186,4 @@ declare class ShopClient {
174
186
  }): Promise<StoreTypeBreakdown>;
175
187
  }
176
188
 
177
- export { CheckoutOperations, Collection, CollectionOperations, Product, ProductOperations, ShopClient, type ShopClientOptions, StoreInfo, StoreOperations, StoreTypeBreakdown };
189
+ export { CheckoutOperations, Collection, CollectionOperations, OpenGraphMeta, Product, ProductOperations, ShopClient, type ShopClientOptions, ShopInfo, ShopOperations, StoreTypeBreakdown };
package/dist/index.mjs CHANGED
@@ -8,9 +8,9 @@ import {
8
8
  createProductOperations
9
9
  } from "./chunk-ZF4M6GMB.mjs";
10
10
  import {
11
- createStoreOperations,
12
- getInfoForStore
13
- } from "./chunk-CUL7ZM2W.mjs";
11
+ createShopOperations,
12
+ getInfoForShop
13
+ } from "./chunk-7IMI76JZ.mjs";
14
14
  import {
15
15
  classifyProduct,
16
16
  determineStoreType,
@@ -353,7 +353,7 @@ var ShopClient = class {
353
353
  if (typeof (options == null ? void 0 : options.cacheTTL) === "number" && options.cacheTTL > 0) {
354
354
  this.cacheExpiry = options.cacheTTL;
355
355
  }
356
- this.storeOperations = createStoreOperations({
356
+ this.shopOperations = createShopOperations({
357
357
  baseUrl: this.baseUrl,
358
358
  storeDomain: this.storeDomain,
359
359
  validateProductExists: this.validateProductExists.bind(this),
@@ -688,7 +688,7 @@ var ShopClient = class {
688
688
  return await this.infoInFlight;
689
689
  }
690
690
  this.infoInFlight = (async () => {
691
- const { info, currencyCode } = await getInfoForStore(
691
+ const { info, currencyCode } = await getInfoForShop(
692
692
  {
693
693
  baseUrl: this.baseUrl,
694
694
  storeDomain: this.storeDomain,
@@ -726,6 +726,60 @@ var ShopClient = class {
726
726
  this.infoCacheValue = void 0;
727
727
  this.infoCacheTimestamp = void 0;
728
728
  }
729
+ /**
730
+ * Fetch OpenGraph metadata from the store homepage.
731
+ * Returns only `og:*` fields without additional parsing or validation.
732
+ */
733
+ async getMetaData(options) {
734
+ try {
735
+ if ((options == null ? void 0 : options.force) === true) {
736
+ this.metaCacheValue = void 0;
737
+ this.metaCacheTimestamp = void 0;
738
+ }
739
+ if (this.metaCacheValue && this.metaCacheTimestamp !== void 0 && Date.now() - this.metaCacheTimestamp < this.cacheExpiry) {
740
+ return this.metaCacheValue;
741
+ }
742
+ if (this.metaInFlight) {
743
+ return await this.metaInFlight;
744
+ }
745
+ this.metaInFlight = (async () => {
746
+ const meta = await this.shopOperations.getMetaData();
747
+ this.metaCacheValue = meta;
748
+ this.metaCacheTimestamp = Date.now();
749
+ return meta;
750
+ })();
751
+ try {
752
+ const result = await this.metaInFlight;
753
+ return result;
754
+ } finally {
755
+ this.metaInFlight = void 0;
756
+ }
757
+ } catch (error) {
758
+ throw this.handleFetchError(
759
+ error,
760
+ "fetching store metadata",
761
+ this.baseUrl
762
+ );
763
+ }
764
+ }
765
+ async getJsonLd() {
766
+ try {
767
+ return await this.shopOperations.getJsonLd();
768
+ } catch (error) {
769
+ throw this.handleFetchError(
770
+ error,
771
+ "fetching store JSON-LD",
772
+ this.baseUrl
773
+ );
774
+ }
775
+ }
776
+ async getHeaderLinks() {
777
+ try {
778
+ return await this.shopOperations.getHeaderLinks();
779
+ } catch (error) {
780
+ throw this.handleFetchError(error, "fetching header links", this.baseUrl);
781
+ }
782
+ }
729
783
  /**
730
784
  * Determine the store's primary vertical and target audience.
731
785
  * Uses `getInfo()` internally; no input required.
@@ -1,4 +1,4 @@
1
- import { StoreInfo } from './store.js';
1
+ import { ShopInfo } from './store.js';
2
2
  import { f as CurrencyCode, P as Product, k as ProductClassification, l as SEOContent, S as ShopifyProduct, a as ShopifySingleProduct } from './types-luPg5O08.js';
3
3
 
4
4
  /**
@@ -77,6 +77,6 @@ interface ProductOperations {
77
77
  /**
78
78
  * Creates product operations for a store instance
79
79
  */
80
- declare function createProductOperations(baseUrl: string, storeDomain: string, fetchProducts: (page: number, limit: number) => Promise<Product[] | null>, productsDto: (products: ShopifyProduct[]) => Product[] | null, productDto: (product: ShopifySingleProduct) => Product, getStoreInfo: () => Promise<StoreInfo>, findProduct: (handle: string) => Promise<Product | null>): ProductOperations;
80
+ declare function createProductOperations(baseUrl: string, storeDomain: string, fetchProducts: (page: number, limit: number) => Promise<Product[] | null>, productsDto: (products: ShopifyProduct[]) => Product[] | null, productDto: (product: ShopifySingleProduct) => Product, getStoreInfo: () => Promise<ShopInfo>, findProduct: (handle: string) => Promise<Product | null>): ProductOperations;
81
81
 
82
82
  export { type ProductOperations, createProductOperations };
package/dist/store.d.ts CHANGED
@@ -4,14 +4,17 @@ import { J as JsonLdEntry, d as CountryDetectionResult, f as CurrencyCode } from
4
4
  * Store operations interface for managing store-related functionality.
5
5
  * Provides methods to fetch comprehensive store information and metadata.
6
6
  */
7
- interface StoreOperations {
8
- info(): Promise<StoreInfo>;
7
+ interface ShopOperations {
8
+ info(): Promise<ShopInfo>;
9
+ getMetaData(): Promise<OpenGraphMeta>;
10
+ getJsonLd(): Promise<JsonLdEntry[] | undefined>;
11
+ getHeaderLinks(): Promise<string[]>;
9
12
  }
10
13
  /**
11
14
  * Comprehensive store information structure returned by the info method.
12
15
  * Contains all metadata, branding, social links, and showcase content for a Shopify store.
13
16
  */
14
- interface StoreInfo {
17
+ interface ShopInfo {
15
18
  name: string;
16
19
  domain: string;
17
20
  slug: string;
@@ -38,17 +41,26 @@ interface StoreInfo {
38
41
  country: CountryDetectionResult["country"];
39
42
  currency: CurrencyCode | null;
40
43
  }
44
+ interface OpenGraphMeta {
45
+ siteName: string | null;
46
+ title: string | null;
47
+ description: string | null;
48
+ url: string | null;
49
+ type: string | null;
50
+ image: string | null;
51
+ imageSecureUrl: string | null;
52
+ }
41
53
  /**
42
54
  * Creates store operations for a ShopClient instance.
43
55
  * @param context - ShopClient context containing necessary methods and properties for store operations
44
56
  */
45
- declare function createStoreOperations(context: {
57
+ declare function createShopOperations(context: {
46
58
  baseUrl: string;
47
59
  storeDomain: string;
48
60
  validateProductExists: (handle: string) => Promise<boolean>;
49
61
  validateCollectionExists: (handle: string) => Promise<boolean>;
50
62
  validateLinksInBatches: <T>(items: T[], validator: (item: T) => Promise<boolean>, batchSize?: number) => Promise<T[]>;
51
63
  handleFetchError: (error: unknown, context: string, url: string) => never;
52
- }): StoreOperations;
64
+ }): ShopOperations;
53
65
 
54
- export { type StoreInfo, type StoreOperations, createStoreOperations };
66
+ export { type OpenGraphMeta, type ShopInfo, type ShopOperations, createShopOperations };
package/dist/store.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
- createStoreOperations
3
- } from "./chunk-CUL7ZM2W.mjs";
2
+ createShopOperations
3
+ } from "./chunk-7IMI76JZ.mjs";
4
4
  import "./chunk-D5MTUWFO.mjs";
5
5
  import "./chunk-G7OCMGA6.mjs";
6
6
  import "./chunk-U3RQRBXZ.mjs";
7
7
  export {
8
- createStoreOperations
8
+ createShopOperations
9
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shop-client",
3
- "version": "3.13.0",
3
+ "version": "3.14.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.mjs",
6
6
  "module": "./dist/index.mjs",