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.
- package/LICENSE +21 -0
- package/README.md +912 -0
- package/dist/checkout.d.mts +31 -0
- package/dist/checkout.d.ts +31 -0
- package/dist/checkout.js +115 -0
- package/dist/checkout.js.map +1 -0
- package/dist/checkout.mjs +7 -0
- package/dist/checkout.mjs.map +1 -0
- package/dist/chunk-2KBOKOAD.mjs +177 -0
- package/dist/chunk-2KBOKOAD.mjs.map +1 -0
- package/dist/chunk-BWKBRM2Z.mjs +136 -0
- package/dist/chunk-BWKBRM2Z.mjs.map +1 -0
- package/dist/chunk-O4BPIIQ6.mjs +503 -0
- package/dist/chunk-O4BPIIQ6.mjs.map +1 -0
- package/dist/chunk-QCTICSBE.mjs +398 -0
- package/dist/chunk-QCTICSBE.mjs.map +1 -0
- package/dist/chunk-QL5OUZGP.mjs +91 -0
- package/dist/chunk-QL5OUZGP.mjs.map +1 -0
- package/dist/chunk-WTK5HUFI.mjs +1287 -0
- package/dist/chunk-WTK5HUFI.mjs.map +1 -0
- package/dist/collections.d.mts +64 -0
- package/dist/collections.d.ts +64 -0
- package/dist/collections.js +540 -0
- package/dist/collections.js.map +1 -0
- package/dist/collections.mjs +9 -0
- package/dist/collections.mjs.map +1 -0
- package/dist/index.d.mts +233 -0
- package/dist/index.d.ts +233 -0
- package/dist/index.js +3241 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +702 -0
- package/dist/index.mjs.map +1 -0
- package/dist/products.d.mts +63 -0
- package/dist/products.d.ts +63 -0
- package/dist/products.js +1206 -0
- package/dist/products.js.map +1 -0
- package/dist/products.mjs +9 -0
- package/dist/products.mjs.map +1 -0
- package/dist/store-CJVUz2Yb.d.mts +608 -0
- package/dist/store-CJVUz2Yb.d.ts +608 -0
- package/dist/store.d.mts +1 -0
- package/dist/store.d.ts +1 -0
- package/dist/store.js +698 -0
- package/dist/store.js.map +1 -0
- package/dist/store.mjs +9 -0
- package/dist/store.mjs.map +1 -0
- package/dist/utils/rate-limit.d.mts +25 -0
- package/dist/utils/rate-limit.d.ts +25 -0
- package/dist/utils/rate-limit.js +203 -0
- package/dist/utils/rate-limit.js.map +1 -0
- package/dist/utils/rate-limit.mjs +11 -0
- package/dist/utils/rate-limit.mjs.map +1 -0
- package/package.json +116 -0
package/README.md
ADDED
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
# Shop Search
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/shop-client)
|
|
4
|
+
[](https://www.typescriptlang.org/)
|
|
5
|
+
[](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.
|