shop-client 3.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +912 -0
  3. package/dist/checkout.d.mts +31 -0
  4. package/dist/checkout.d.ts +31 -0
  5. package/dist/checkout.js +115 -0
  6. package/dist/checkout.js.map +1 -0
  7. package/dist/checkout.mjs +7 -0
  8. package/dist/checkout.mjs.map +1 -0
  9. package/dist/chunk-2KBOKOAD.mjs +177 -0
  10. package/dist/chunk-2KBOKOAD.mjs.map +1 -0
  11. package/dist/chunk-BWKBRM2Z.mjs +136 -0
  12. package/dist/chunk-BWKBRM2Z.mjs.map +1 -0
  13. package/dist/chunk-O4BPIIQ6.mjs +503 -0
  14. package/dist/chunk-O4BPIIQ6.mjs.map +1 -0
  15. package/dist/chunk-QCTICSBE.mjs +398 -0
  16. package/dist/chunk-QCTICSBE.mjs.map +1 -0
  17. package/dist/chunk-QL5OUZGP.mjs +91 -0
  18. package/dist/chunk-QL5OUZGP.mjs.map +1 -0
  19. package/dist/chunk-WTK5HUFI.mjs +1287 -0
  20. package/dist/chunk-WTK5HUFI.mjs.map +1 -0
  21. package/dist/collections.d.mts +64 -0
  22. package/dist/collections.d.ts +64 -0
  23. package/dist/collections.js +540 -0
  24. package/dist/collections.js.map +1 -0
  25. package/dist/collections.mjs +9 -0
  26. package/dist/collections.mjs.map +1 -0
  27. package/dist/index.d.mts +233 -0
  28. package/dist/index.d.ts +233 -0
  29. package/dist/index.js +3241 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/index.mjs +702 -0
  32. package/dist/index.mjs.map +1 -0
  33. package/dist/products.d.mts +63 -0
  34. package/dist/products.d.ts +63 -0
  35. package/dist/products.js +1206 -0
  36. package/dist/products.js.map +1 -0
  37. package/dist/products.mjs +9 -0
  38. package/dist/products.mjs.map +1 -0
  39. package/dist/store-CJVUz2Yb.d.mts +608 -0
  40. package/dist/store-CJVUz2Yb.d.ts +608 -0
  41. package/dist/store.d.mts +1 -0
  42. package/dist/store.d.ts +1 -0
  43. package/dist/store.js +698 -0
  44. package/dist/store.js.map +1 -0
  45. package/dist/store.mjs +9 -0
  46. package/dist/store.mjs.map +1 -0
  47. package/dist/utils/rate-limit.d.mts +25 -0
  48. package/dist/utils/rate-limit.d.ts +25 -0
  49. package/dist/utils/rate-limit.js +203 -0
  50. package/dist/utils/rate-limit.js.map +1 -0
  51. package/dist/utils/rate-limit.mjs +11 -0
  52. package/dist/utils/rate-limit.mjs.map +1 -0
  53. package/package.json +116 -0
package/README.md ADDED
@@ -0,0 +1,912 @@
1
+ # Shop Search
2
+
3
+ [![npm version](https://badge.fury.io/js/shop-client.svg)](https://badge.fury.io/js/shop-client)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
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
+
9
+ ## πŸš€ Features
10
+
11
+ - **Complete Store Data Access**: Fetch products, collections, and store information
12
+ - **Flexible Product Retrieval**: Get all products, paginated results, or find specific items
13
+ - **Collection Management**: Access collections and their associated products
14
+ - **Checkout Integration**: Generate pre-filled checkout URLs
15
+ - **Type-Safe**: Written in TypeScript with comprehensive type definitions
16
+ - **Performance Optimized**: Efficient data fetching with built-in error handling
17
+ - **Zero Dependencies**: Lightweight with minimal external dependencies
18
+ - **Store Type Classification**: Infers audience and verticals from showcased products (body_html-only)
19
+
20
+ ## πŸ“¦ Installation
21
+
22
+ ```bash
23
+ npm install shop-client
24
+ ```
25
+
26
+ ```bash
27
+ yarn add shop-client
28
+ ```
29
+
30
+ ```bash
31
+ pnpm add shop-client
32
+ ```
33
+
34
+ ## πŸ”§ Quick Start
35
+
36
+ ```typescript
37
+ import { ShopClient } from 'shop-client';
38
+
39
+ // Initialize shop client instance
40
+ const shop = new ShopClient("your-store-domain.com");
41
+
42
+ // Fetch store information
43
+ const storeInfo = await shop.getInfo();
44
+
45
+ // Fetch all products
46
+ const products = await shop.products.all();
47
+
48
+ // Find specific product
49
+ const product = await shop.products.find("product-handle");
50
+ ```
51
+
52
+ ## Browser Usage
53
+
54
+ Some in-browser environments load npm packages via blob URLs and can error if the content is not served with a JavaScript MIME type (e.g., β€œModules must be served with a valid MIME type like application/javascript”). To use `shop-client` directly in the browser, import the ESM build from a CDN that sets the correct `Content-Type`:
55
+
56
+ ```html
57
+ <!-- Import map to pin shop-client to a CDN ESM URL -->
58
+ <script type="importmap">
59
+ {
60
+ "imports": {
61
+ "shop-client": "https://cdn.jsdelivr.net/npm/shop-client/+esm"
62
+ }
63
+ }
64
+ </script>
65
+
66
+ <script type="module">
67
+ import { ShopClient } from 'shop-client';
68
+ const shop = new ShopClient('https://example.myshopify.com/');
69
+ const info = await shop.getInfo();
70
+ console.log(info);
71
+ const products = await shop.products.paginated({ page: 1, limit: 24 });
72
+ console.log(products);
73
+ // For collections:
74
+ const colProducts = await shop.collections.products.paginated('mens', { page: 1, limit: 24 });
75
+ console.log(colProducts);
76
+ }</script>
77
+ ```
78
+
79
+ Alternative CDN URLs:
80
+ - `https://esm.sh/shop-client@3.5.0`
81
+ - `https://unpkg.com/shop-client@3.5.0?module`
82
+
83
+ Troubleshooting:
84
+ - Ensure the CDN returns `Content-Type: application/javascript`. The `+esm` and `?module` suffixes enforce ESM delivery.
85
+ - If you build your own blob, set `new Blob(code, { type: 'text/javascript' })` before `import()`.
86
+ - For app frameworks (Vite, Next.js), import `shop-client` normally and let the bundler serve modules.
87
+
88
+ ## Server/Edge Usage
89
+
90
+ For robust production setups, run `shop-client` on the server or an edge function and return JSON to the browser:
91
+
92
+ ```ts
93
+ // /api/shop-info.ts (Edge/Node)
94
+ import { ShopClient } from 'shop-client';
95
+
96
+ export default async function handler(req, res) {
97
+ const shop = new ShopClient('https://example.myshopify.com/');
98
+ const info = await shop.getInfo();
99
+ res.json(info);
100
+ }
101
+ ```
102
+
103
+ Client:
104
+
105
+ ```ts
106
+ const info = await fetch('/api/shop-info').then(r => r.json());
107
+ ```
108
+
109
+ ## Deep Imports & Tree-Shaking
110
+
111
+ For optimal bundle size, import only what you need using subpath exports. The library ships ESM/CJS builds and declares `sideEffects: false` for better dead code elimination.
112
+
113
+ Examples:
114
+
115
+ ```typescript
116
+ // ESM deep imports
117
+ import { configureRateLimit } from 'shop-client/rate-limit';
118
+ import { ProductOperations } from 'shop-client/products';
119
+
120
+ // CommonJS deep imports
121
+ const { configureRateLimit } = require('shop-client/rate-limit');
122
+ const { ProductOperations } = require('shop-client/products');
123
+
124
+ // Recommended: import specific functions you use
125
+ import { ShopClient } from 'shop-client';
126
+ import { fetchProducts } from 'shop-client/products';
127
+ ```
128
+
129
+ Notes:
130
+ - ESM consumers (Vite/Rollup/esbuild) get tree-shaking out of the box.
131
+ - Webpack benefits from `sideEffects: false`; avoid importing wide barrels when possible.
132
+ - Rate limiter timer starts lazily on first use (no import-time side effects).
133
+
134
+ ### Migration: Barrel β†’ Subpath Imports
135
+
136
+ 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
+
138
+ Examples:
139
+
140
+ ```typescript
141
+ // Before (barrel import)
142
+ import { ShopClient, configureRateLimit } from 'shop-client';
143
+
144
+ // After (deep imports for better tree-shaking)
145
+ import { ShopClient } from 'shop-client';
146
+ import { configureRateLimit } from 'shop-client/rate-limit';
147
+
148
+ // Feature-specific imports
149
+ import { fetchProducts } from 'shop-client/products';
150
+ import { createCheckoutOperations } from 'shop-client/checkout';
151
+ ```
152
+
153
+ CommonJS:
154
+
155
+ ```javascript
156
+ // Before
157
+ const { ShopClient, configureRateLimit } = require('shop-client');
158
+
159
+ // After
160
+ const { ShopClient } = require('shop-client');
161
+ const { configureRateLimit } = require('shop-client/rate-limit');
162
+ ```
163
+
164
+ Notes:
165
+ - No bundler changes required; deep imports are exposed via `exports`.
166
+ - The root entry continues to work; prefer deep imports for production apps.
167
+
168
+ ## ��️ Rate Limiting
169
+
170
+ `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
+
172
+ - Default: disabled
173
+ - When enabled: defaults to `5` requests per `1000ms` with max concurrency `5`
174
+ - Configure globally via `configureRateLimit`
175
+
176
+ ```typescript
177
+ import { ShopClient, configureRateLimit } from 'shop-client';
178
+
179
+ // Enable and configure the global rate limiter
180
+ configureRateLimit({
181
+ enabled: true,
182
+ maxRequestsPerInterval: 60, // 60 requests
183
+ intervalMs: 60_000, // per minute
184
+ maxConcurrency: 4, // up to 4 in parallel
185
+ });
186
+
187
+ const shop = new ShopClient("your-store-domain.com");
188
+
189
+ // All subsequent library calls use the limiter automatically
190
+ const products = await shop.products.all();
191
+ ```
192
+
193
+ Notes:
194
+ - The limiter is global to the process. Call `configureRateLimit` once at startup.
195
+ - If you are crawling multiple stores, prefer lower concurrency and a longer interval to reduce pressure.
196
+ - When disabled, the library uses native `fetch` without throttling.
197
+
198
+ ### Advanced: Per-Host and Per-Class Limits
199
+
200
+ You can set different buckets by host (including wildcards) or by logical class:
201
+
202
+ ```typescript
203
+ import { configureRateLimit } from 'shop-client';
204
+
205
+ configureRateLimit({
206
+ enabled: true,
207
+ // Default fallback
208
+ maxRequestsPerInterval: 10,
209
+ intervalMs: 1000,
210
+ maxConcurrency: 5,
211
+
212
+ // Host-specific buckets (exact host or wildcard suffix '*.example.com')
213
+ perHost: {
214
+ 'openrouter.ai': { maxRequestsPerInterval: 2, intervalMs: 1000, maxConcurrency: 1 },
215
+ '*.myshopify.com': { maxRequestsPerInterval: 5, intervalMs: 1000, maxConcurrency: 3 },
216
+ 'your-store-domain.com': { maxRequestsPerInterval: 8, intervalMs: 1000, maxConcurrency: 4 },
217
+ },
218
+
219
+ // Class-specific buckets (use by passing `rateLimitClass` in RequestInit)
220
+ perClass: {
221
+ openrouter: { maxRequestsPerInterval: 2, intervalMs: 1000, maxConcurrency: 1 },
222
+ shopify: { maxRequestsPerInterval: 6, intervalMs: 1000, maxConcurrency: 3 },
223
+ },
224
+ });
225
+
226
+ // If you make custom fetches, you can tag them with a class:
227
+ // await rateLimitedFetch(url, { rateLimitClass: 'openrouter' });
228
+ ```
229
+
230
+ Resolution order:
231
+ - If `rateLimitClass` is present, that bucket is used.
232
+ - Else, a matching `perHost` bucket is used (exact match first, then wildcard suffix).
233
+ - Else, the global default bucket is used.
234
+
235
+ Tip: You can deep import the limiter configuration surface:
236
+
237
+ ```typescript
238
+ import { configureRateLimit } from 'shop-client/rate-limit';
239
+ ```
240
+
241
+ ## πŸ“š API Reference
242
+
243
+ ### Store Information
244
+
245
+ #### `getInfo()`
246
+
247
+ Fetches comprehensive store metadata including branding, social links, and featured content.
248
+
249
+ ```typescript
250
+ const storeInfo = await shop.getInfo();
251
+ ```
252
+
253
+ **Returns:** `StoreInfo` object containing:
254
+ - `name`: Store name from meta tags
255
+ - `title`: Store title
256
+ - `description`: Store description
257
+ - `domain`: Store domain
258
+ - `slug`: Generated store slug
259
+ - `logoUrl`: Store logo URL
260
+ - `socialLinks`: Social media URLs (Facebook, Instagram, etc.)
261
+ - `contactLinks`: Contact information (phone, email, contact page)
262
+ - `headerLinks`: Navigation menu links
263
+ - `showcase`: Featured products and collections
264
+ - `jsonLdData`: Structured data from the store
265
+
266
+ ### Products
267
+
268
+ #### `products.all()`
269
+
270
+ Fetches all products from the store with automatic pagination handling.
271
+
272
+ ```typescript
273
+ const allProducts = await shop.products.all();
274
+ ```
275
+
276
+ **Returns:** `Product[]` - Array of all products in the store
277
+
278
+ #### `products.paginated(options)`
279
+
280
+ Fetches products with manual pagination control.
281
+
282
+ ```typescript
283
+ const products = await shop.products.paginated({
284
+ page: 1,
285
+ limit: 25,
286
+ // Optional currency override aligned with Intl.NumberFormat
287
+ currency: "EUR",
288
+ });
289
+ ```
290
+
291
+ **Parameters:**
292
+ - `page` (number, optional): Page number (default: 1)
293
+ - `limit` (number, optional): Products per page (default: 250, max: 250)
294
+ - `currency` (CurrencyCode, optional): ISO 4217 code aligned with `Intl.NumberFormatOptions['currency']` (e.g., `"USD"`, `"EUR"`, `"JPY"`)
295
+
296
+ **Returns:** `Product[]` - Array of products for the specified page
297
+
298
+ #### `products.find(handle)`
299
+
300
+ Finds a specific product by its handle.
301
+
302
+ ```typescript
303
+ const product = await shop.products.find("product-handle");
304
+
305
+ // With currency override
306
+ const productEur = await shop.products.find("product-handle", { currency: "EUR" });
307
+ ```
308
+
309
+ **Parameters:**
310
+ - `handle` (string): The product handle/slug
311
+ - `options` (object, optional): Additional options
312
+ - `currency` (CurrencyCode, optional): ISO 4217 code aligned with `Intl.NumberFormatOptions['currency']`
313
+
314
+ **Returns:** `Product | null` - Product object or null if not found
315
+
316
+ #### `products.showcased()`
317
+
318
+ Fetches products featured on the store's homepage.
319
+
320
+ ```typescript
321
+ const showcasedProducts = await shop.products.showcased();
322
+ ```
323
+
324
+ **Returns:** `Product[]` - Array of featured products
325
+
326
+ #### `products.filter()`
327
+
328
+ Creates a map of variant options and their distinct values from all products in the store. This is useful for building filter interfaces, search facets, and product option selectors.
329
+
330
+ ```typescript
331
+ const filters = await shop.products.filter();
332
+ console.log('Available filters:', filters);
333
+
334
+ // Example output:
335
+ // {
336
+ // "size": ["small", "medium", "large", "xl"],
337
+ // "color": ["black", "blue", "red", "white"],
338
+ // "material": ["cotton", "polyester", "wool"]
339
+ // }
340
+
341
+ // Use filters for UI components
342
+ Object.entries(filters || {}).forEach(([optionName, values]) => {
343
+ console.log(`${optionName}: ${values.join(', ')}`);
344
+ });
345
+ ```
346
+
347
+ **Returns:** `Record<string, string[]> | null` - Object mapping option names to arrays of their unique values (all lowercase), or null if error occurs
348
+
349
+ **Features:**
350
+ - Processes all products across all pages automatically
351
+ - Returns lowercase, unique values for consistency
352
+ - Handles products with multiple variant options
353
+ - Returns empty object `{}` if no products have variants
354
+
355
+ ### Collections
356
+
357
+ #### `collections.all()`
358
+
359
+ Fetches all collections from the store.
360
+
361
+ ```typescript
362
+ const collections = await shop.collections.all();
363
+ ```
364
+
365
+ **Returns:** `Collection[]` - Array of all collections
366
+
367
+ #### `collections.find(handle)`
368
+
369
+ Finds a specific collection by its handle.
370
+
371
+ ```typescript
372
+ const collection = await shop.collections.find("collection-handle");
373
+ ```
374
+
375
+ **Parameters:**
376
+ - `handle` (string): The collection handle/slug
377
+
378
+ **Returns:** `Collection | null` - Collection object or null if not found
379
+
380
+ #### `collections.showcased()`
381
+
382
+ Fetches collections featured on the store's homepage.
383
+
384
+ ```typescript
385
+ const showcasedCollections = await shop.collections.showcased();
386
+ ```
387
+
388
+ **Returns:** `Collection[]` - Array of featured collections
389
+
390
+ #### `collections.paginated(options)`
391
+
392
+ Fetches collections with manual pagination control.
393
+
394
+ ```typescript
395
+ const collectionsPage = await shop.collections.paginated({
396
+ page: 1,
397
+ limit: 10,
398
+ });
399
+ ```
400
+
401
+ **Parameters:**
402
+ - `page` (number, optional): Page number (default: 1)
403
+ - `limit` (number, optional): Collections per page (default: 10, max: 250)
404
+
405
+ **Returns:** `Collection[]` - Array of collections for the specified page
406
+
407
+ ### Collection Products
408
+
409
+ #### `collections.products.all(handle)`
410
+
411
+ Fetches all products from a specific collection.
412
+
413
+ ```typescript
414
+ const products = await shop.collections.products.all("collection-handle");
415
+ ```
416
+
417
+ **Parameters:**
418
+ - `handle` (string): The collection handle
419
+
420
+ **Returns:** `Product[] | null` - Array of products in the collection
421
+
422
+ #### `collections.products.paginated(handle, options)`
423
+
424
+ Fetches products from a collection with pagination.
425
+
426
+ ```typescript
427
+ const products = await shop.collections.products.paginated("collection-handle", {
428
+ page: 1,
429
+ limit: 25,
430
+ currency: "GBP",
431
+ });
432
+ ```
433
+
434
+ **Parameters:**
435
+ - `handle` (string): The collection handle
436
+ - `options` (object): Pagination options
437
+ - `page` (number, optional): Page number (default: 1)
438
+ - `limit` (number, optional): Products per page (default: 250)
439
+ - `currency` (CurrencyCode, optional): ISO 4217 code aligned with `Intl.NumberFormatOptions['currency']`
440
+
441
+ **Returns:** `Product[]` - Array of products for the specified page
442
+
443
+ #### Currency Override
444
+
445
+ By default, pricing is formatted using the store’s detected currency.
446
+ You can override the currency for product and collection queries by passing a `currency` option.
447
+ This override updates `Product.currency` and `Product.localizedPricing.currency` (and related formatted strings) only.
448
+
449
+ ```typescript
450
+ // Products
451
+ await shop.products.paginated({ page: 1, limit: 25, currency: "EUR" });
452
+ await shop.products.all({ currency: "JPY" });
453
+ await shop.products.find("product-handle", { currency: "GBP" });
454
+
455
+ // Collection products
456
+ await shop.collections.products.paginated("collection-handle", { page: 1, limit: 25, currency: "CAD" });
457
+ await shop.collections.products.all("collection-handle", { currency: "AUD" });
458
+ ```
459
+
460
+ Type: `CurrencyCode` is defined as `NonNullable<Intl.NumberFormatOptions['currency']>`.
461
+ This ensures compatibility with `Intl.NumberFormat` and avoids maintaining a hardcoded list.
462
+
463
+ ### Checkout
464
+
465
+ #### `checkout.createUrl(params)`
466
+
467
+ Generates a Shopify checkout URL with pre-filled customer information and cart items.
468
+
469
+ ```typescript
470
+ const checkoutUrl = shop.checkout.createUrl({
471
+ email: "customer@example.com",
472
+ items: [
473
+ { productVariantId: "variant-id-1", quantity: "2" },
474
+ { productVariantId: "variant-id-2", quantity: "1" }
475
+ ],
476
+ address: {
477
+ firstName: "John",
478
+ lastName: "Doe",
479
+ address1: "123 Main St",
480
+ city: "Anytown",
481
+ zip: "12345",
482
+ country: "USA",
483
+ province: "CA",
484
+ phone: "123-456-7890"
485
+ }
486
+ });
487
+ ```
488
+
489
+ **Parameters:**
490
+ - `email` (string): Customer's email address
491
+ - `items` (array): Cart items with `productVariantId` and `quantity`
492
+ - `address` (object): Shipping address details
493
+
494
+ **Returns:** `string` - Complete checkout URL
495
+
496
+ ### Utilities
497
+
498
+ Helper utilities exported for common normalization and parsing tasks.
499
+
500
+ ```typescript
501
+ import { sanitizeDomain, safeParseDate } from 'shop-client';
502
+
503
+ // Normalize domains safely
504
+ sanitizeDomain('https://www.example.com'); // "example.com"
505
+ sanitizeDomain('www.example.com', { stripWWW: false }); // "www.example.com"
506
+ sanitizeDomain('http://example.com/path'); // "example.com"
507
+
508
+ // Errors on invalid input (e.g., bare hostname without suffix)
509
+ try {
510
+ sanitizeDomain('example'); // throws
511
+ } catch (e) {
512
+ console.error('Invalid domain');
513
+ }
514
+
515
+ // Safely parse dates (avoids Invalid Date)
516
+ safeParseDate('2024-10-31T12:34:56Z'); // Date
517
+ safeParseDate(''); // undefined
518
+ safeParseDate('not-a-date'); // undefined
519
+ ```
520
+
521
+ Notes:
522
+ - `sanitizeDomain` trims protocols, paths, and optional `www.` depending on `stripWWW`.
523
+ - Throws for invalid inputs: empty strings or hostnames missing a public suffix (e.g., `example`).
524
+ - `safeParseDate` returns `undefined` for invalid inputs; product `publishedAt` may be `null` when unavailable.
525
+
526
+ #### Release and Publishing
527
+
528
+ - Releases are automated via `semantic-release` and npm Trusted Publishing.
529
+ - The release workflow uses Node.js `22.14.0` to satisfy `semantic-release` requirements.
530
+ - npm publishes use OIDC with provenance; no `NPM_TOKEN` secret is required.
531
+ - Ensure your npm package settings add this GitHub repo as a trusted publisher and set the environment name to `npm-publish`.
532
+
533
+ ### Store Type Classification
534
+
535
+ Determine the store’s primary verticals and target audiences using showcased products. Classification uses only each product’s `body_html` content and aggregates per-product results, optionally pruned by store-level signals.
536
+
537
+ ```typescript
538
+ import { ShopClient } from 'shop-client';
539
+
540
+ const shop = new ShopClient('your-store-domain.com');
541
+
542
+ const breakdown = await shop.determineStoreType({
543
+ // Optional: provide an OpenRouter API key for online classification
544
+ // Offline mode falls back to regex heuristics if no key is set
545
+ apiKey: process.env.OPENROUTER_API_KEY,
546
+ // Optional: model name when using online classification
547
+ model: 'openai/gpt-4o-mini',
548
+ // Optional: limit the number of showcased products sampled (default 10, max 50)
549
+ maxShowcaseProducts: 12,
550
+ // Note: showcased collections are not used for classification
551
+ maxShowcaseCollections: 0,
552
+ });
553
+
554
+ // Example breakdown shape
555
+ // {
556
+ // generic: { accessories: ['general'] },
557
+ // adult_female: { clothing: ['dresses', 'tops'] }
558
+ // }
559
+ ```
560
+
561
+ Details:
562
+ - Uses only `product.bodyHtml` for classification (no images or external text).
563
+ - Samples up to `maxShowcaseProducts` from `getInfo().showcase.products`.
564
+ - Aggregates per-product audience/vertical into a multi-audience breakdown.
565
+ - If `OPENROUTER_API_KEY` is absent or `OPENROUTER_OFFLINE=1`, uses offline regex heuristics.
566
+ - Applies store-level pruning based on title/description to improve consistency.
567
+
568
+ ## πŸ—οΈ Type Definitions
569
+
570
+ ### StoreInfo
571
+
572
+ ```typescript
573
+ type StoreInfo = {
574
+ name: string;
575
+ domain: string;
576
+ slug: string;
577
+ title: string | null;
578
+ description: string | null;
579
+ shopifyWalletId: string | null;
580
+ myShopifySubdomain: string | null;
581
+ logoUrl: string | null;
582
+ socialLinks: Record<string, string>;
583
+ contactLinks: {
584
+ tel: string | null;
585
+ email: string | null;
586
+ contactPage: string | null;
587
+ };
588
+ headerLinks: string[];
589
+ showcase: {
590
+ products: string[];
591
+ collections: string[];
592
+ };
593
+ jsonLdData: any[] | null;
594
+ };
595
+ ```
596
+
597
+ ### Product
598
+
599
+ ```typescript
600
+ type Product = {
601
+ slug: string;
602
+ handle: string;
603
+ platformId: string;
604
+ title: string;
605
+ available: boolean;
606
+ price: number;
607
+ priceMin: number;
608
+ priceVaries: boolean;
609
+ compareAtPrice: number;
610
+ compareAtPriceMin: number;
611
+ priceMax: number;
612
+ compareAtPriceMax: number;
613
+ compareAtPriceVaries: boolean;
614
+ discount: number;
615
+ currency?: string;
616
+ options: ProductOption[];
617
+ bodyHtml: string | null;
618
+ active?: boolean;
619
+ productType: string | null;
620
+ tags: string[];
621
+ vendor: string;
622
+ featuredImage?: string | null;
623
+ isProxyFeaturedImage: boolean | null;
624
+ createdAt?: Date;
625
+ updatedAt?: Date;
626
+ variants: ProductVariant[] | null;
627
+ images: ProductImage[];
628
+ publishedAt: Date | null;
629
+ seo?: MetaTag[] | null;
630
+ metaTags?: MetaTag[] | null;
631
+ displayScore?: number;
632
+ deletedAt?: Date | null;
633
+ storeSlug: string;
634
+ storeDomain: string;
635
+ embedding?: number[] | null;
636
+ url: string;
637
+ requiresSellingPlan?: boolean | null;
638
+ sellingPlanGroups?: unknown;
639
+ // Keys formatted as name#value parts joined by '##' (alphabetically sorted), e.g., "color#blue##size#xl"
640
+ variantOptionsMap: Record<string, string>;
641
+ };
642
+
643
+ #### Date Handling
644
+
645
+ - `createdAt` and `updatedAt` are parsed using a safe parser and may be `undefined` when source values are empty or invalid.
646
+ - `publishedAt` is `Date | null` and will be `null` when unavailable or invalid.
647
+
648
+ #### Variant Options Map
649
+
650
+ - Each product includes `variantOptionsMap: Record<string, string>` when variants are present.
651
+ - Keys are composed of normalized option name/value pairs in the form `name#value`, joined by `##` and sorted alphabetically for stability.
652
+ - Example: `{ "color#blue##size#xl": "123", "color#red##size#m": "456" }`.
653
+ - Normalization uses `normalizeKey` (lowercases; spaces β†’ `_`; non-space separators like `-` remain intact).
654
+ ```
655
+
656
+ ### ProductVariant
657
+
658
+ ```typescript
659
+ type ProductVariant = {
660
+ id: string;
661
+ platformId: string;
662
+ name?: string;
663
+ title: string;
664
+ option1: string | null;
665
+ option2: string | null;
666
+ option3: string | null;
667
+ options?: string[];
668
+ sku: string | null;
669
+ requiresShipping: boolean;
670
+ taxable: boolean;
671
+ featuredImage: ProductVariantImage | null;
672
+ available: boolean;
673
+ price: number;
674
+ weightInGrams?: number;
675
+ compareAtPrice: number;
676
+ position: number;
677
+ productId: number;
678
+ createdAt?: string;
679
+ updatedAt?: string;
680
+ };
681
+ ```
682
+
683
+ ### ProductVariantImage
684
+
685
+ ```typescript
686
+ type ProductVariantImage = {
687
+ id: number;
688
+ src: string;
689
+ position: number;
690
+ productId: number;
691
+ aspectRatio: number;
692
+ variantIds: unknown[];
693
+ createdAt: string;
694
+ updatedAt: string;
695
+ alt: string | null;
696
+ width: number;
697
+ height: number;
698
+ };
699
+ ```
700
+
701
+ ### ProductImage
702
+
703
+ ```typescript
704
+ type ProductImage = {
705
+ id: number;
706
+ productId: number;
707
+ alt: string | null;
708
+ position: number;
709
+ src: string;
710
+ mediaType: "image" | "video";
711
+ variantIds: unknown[];
712
+ createdAt?: string;
713
+ updatedAt?: string;
714
+ width: number;
715
+ height: number;
716
+ aspect_ratio?: number;
717
+ };
718
+ ```
719
+
720
+ ### ProductOption
721
+
722
+ ```typescript
723
+ type ProductOption = {
724
+ key: string;
725
+ data: string[];
726
+ name: string;
727
+ position: number;
728
+ values: string[];
729
+ };
730
+ ```
731
+
732
+ ### Collection
733
+
734
+ ```typescript
735
+ type Collection = {
736
+ id: string;
737
+ title: string;
738
+ handle: string;
739
+ description?: string;
740
+ image?: {
741
+ id: number;
742
+ createdAt: string;
743
+ src: string;
744
+ alt?: string;
745
+ };
746
+ productsCount: number;
747
+ publishedAt: string;
748
+ updatedAt: string;
749
+ };
750
+ ```
751
+
752
+ ### MetaTag
753
+
754
+ ```typescript
755
+ type MetaTag =
756
+ | { name: string; content: string }
757
+ | { property: string; content: string }
758
+ | { itemprop: string; content: string };
759
+ ```
760
+
761
+ ## πŸ’‘ Use Cases
762
+
763
+ ### E-commerce Applications
764
+ - Build product catalogs and search functionality
765
+ - Create comparison shopping tools
766
+ - Develop inventory management systems
767
+
768
+ ### Data Analysis
769
+ - Analyze product pricing trends
770
+ - Monitor competitor stores
771
+ - Generate market research reports
772
+
773
+ ### Marketing Tools
774
+ - Create automated product feeds
775
+ - Build recommendation engines
776
+ - Generate SEO-optimized product pages
777
+
778
+ ### Integration Examples
779
+ - Sync products with external databases
780
+ - Create custom checkout flows
781
+ - Build headless commerce solutions
782
+
783
+ ## πŸ” Advanced Examples
784
+
785
+ ### Building a Product Search
786
+
787
+ ```typescript
788
+ async function searchProducts(shop: ShopClient, query: string) {
789
+ const allProducts = await shop.products.all();
790
+ return allProducts.filter(product =>
791
+ product.title.toLowerCase().includes(query.toLowerCase()) ||
792
+ product.tags.some(tag => tag.toLowerCase().includes(query.toLowerCase()))
793
+ );
794
+ }
795
+ ```
796
+
797
+ ### Price Monitoring
798
+
799
+ ```typescript
800
+ async function monitorPrices(shop: ShopClient) {
801
+ const products = await shop.products.all();
802
+ return products.map(product => ({
803
+ handle: product.handle,
804
+ title: product.title,
805
+ currentPrice: product.price,
806
+ originalPrice: product.compareAtPrice,
807
+ discount: product.discount,
808
+ onSale: product.compareAtPrice > product.price
809
+ }));
810
+ }
811
+ ```
812
+
813
+ ### Collection Analysis
814
+
815
+ ```typescript
816
+ async function analyzeCollections(shop: ShopClient) {
817
+ const collections = await shop.collections.all();
818
+ const analysis = [];
819
+
820
+ for (const collection of collections) {
821
+ const products = await shop.collections.products.all(collection.handle);
822
+ if (products) {
823
+ analysis.push({
824
+ name: collection.title,
825
+ productCount: products.length,
826
+ averagePrice: products.reduce((sum, p) => sum + p.price, 0) / products.length,
827
+ priceRange: {
828
+ min: Math.min(...products.map(p => p.price)),
829
+ max: Math.max(...products.map(p => p.price))
830
+ }
831
+ });
832
+ }
833
+ }
834
+
835
+ return analysis;
836
+ }
837
+ ```
838
+
839
+ ## πŸ€– LLM Integration
840
+
841
+ This package is designed to be LLM-friendly with comprehensive documentation and structured APIs:
842
+
843
+ ### For AI Code Generation
844
+ - **Complete Type Safety**: Full TypeScript definitions enable accurate code completion and generation
845
+ - **Predictable API Patterns**: Consistent method naming and return types across all operations
846
+ - **Comprehensive Examples**: Real-world usage patterns in `/examples` directory
847
+ - **Detailed Documentation**: Technical context in `/.llm` directory for AI understanding
848
+
849
+ ### For E-commerce AI Applications
850
+ - **Rich Product Data**: Complete product information including variants, pricing, and metadata
851
+ - **Structured Store Information**: Organized store data perfect for AI analysis and recommendations
852
+ - **Search-Ready Data**: Product tags, descriptions, and categories optimized for semantic search
853
+ - **Batch Operations**: Efficient data fetching for large-scale AI processing
854
+
855
+ ### LLM-Friendly Resources
856
+ - [`llm.txt`](./llm.txt) - Complete repository overview and API surface
857
+ - [`/.llm/context.md`](./.llm/context.md) - Technical architecture and implementation details
858
+ - [`/.llm/api-reference.md`](./.llm/api-reference.md) - Comprehensive API documentation with examples
859
+ - [`/.llm/examples.md`](./.llm/examples.md) - Code patterns and usage examples
860
+ - [`ARCHITECTURE.md`](./ARCHITECTURE.md) - System design and extension points
861
+ - [`CONTRIBUTING.md`](./CONTRIBUTING.md) - Development guidelines and best practices
862
+
863
+ ### AI Use Cases
864
+ - **Product Recommendation Systems**: Rich product data with relationships and metadata
865
+ - **Price Monitoring**: Automated price tracking and comparison tools
866
+ - **Inventory Analysis**: Stock level monitoring and trend analysis
867
+ - **Content Generation**: Product descriptions and marketing content creation
868
+ - **Market Research**: Competitive analysis and market trend identification
869
+
870
+ ### Keywords for LLM Discovery
871
+ `shopify`, `ecommerce`, `product-data`, `store-scraping`, `typescript`, `nodejs`, `api-client`, `product-catalog`, `checkout`, `collections`, `variants`, `pricing`, `inventory`, `headless-commerce`, `ai-ready`, `llm-friendly`, `semantic-search`, `product-recommendations`, `price-monitoring`
872
+
873
+ ## πŸ› οΈ Error Handling
874
+
875
+ The library includes comprehensive error handling:
876
+
877
+ ```typescript
878
+ try {
879
+ const product = await shop.products.find("non-existent-handle");
880
+ // Returns null for not found
881
+ } catch (error) {
882
+ // Handles network errors, invalid domains, etc.
883
+ console.error('Error fetching product:', error.message);
884
+ }
885
+ ```
886
+
887
+ ## πŸ” Security and Dependency Overrides
888
+
889
+ - This project pins vulnerable transitive dependencies using npm `overrides` to keep CI/security scans green.
890
+ - We currently force `glob` to `11.1.0` to avoid the CLI command injection vulnerability affecting `glob@10.3.7–11.0.3`.
891
+ - The library does not use the `glob` CLI; pinning removes audit warnings without impacting functionality.
892
+ - If scanners flag new CVEs, update `package.json` `overrides` and reinstall dependencies.
893
+
894
+ ## βœ… Parsing Reliability Notes
895
+
896
+ - Contact link parsing is hardened to correctly detect:
897
+ - `tel:` phone links
898
+ - `mailto:` email links
899
+ - `contactPage` URLs (e.g., `/pages/contact`)
900
+ - Tests cover protocol-relative social links normalization and contact page detection to prevent regressions.
901
+
902
+ ## πŸ“„ License
903
+
904
+ MIT License - see the [LICENSE](LICENSE) file for details.
905
+
906
+ ## 🀝 Contributing
907
+
908
+ Contributions are welcome! Please feel free to submit a Pull Request.
909
+
910
+ ## πŸ“ž Support
911
+
912
+ For questions and support, please open an issue on the GitHub repository.