shop-client 3.9.0 → 3.9.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/README.md +65 -0
- package/dist/ai/enrich.d.ts +93 -0
- package/dist/ai/enrich.js +25 -0
- package/dist/checkout.js +6 -0
- package/dist/chunk-2MF53V33.js +196 -0
- package/dist/chunk-CN7L3BHG.js +147 -0
- package/dist/chunk-CXUCPK6X.js +460 -0
- package/dist/chunk-DJQEZNHG.js +233 -0
- package/dist/chunk-MOBWPEY4.js +420 -0
- package/dist/chunk-QUDGES3A.js +195 -0
- package/dist/chunk-RR6YTQWP.js +90 -0
- package/dist/chunk-VPPCOJC3.js +865 -0
- package/dist/collections.d.ts +2 -1
- package/dist/collections.js +8 -0
- package/dist/index.d.ts +7 -84
- package/dist/index.js +753 -0
- package/dist/products.d.ts +2 -1
- package/dist/products.js +8 -0
- package/dist/store.d.ts +53 -1
- package/dist/store.js +9 -0
- package/dist/{store-iQARl6J3.d.ts → types-luPg5O08.d.ts} +1 -208
- package/dist/utils/detect-country.d.ts +32 -0
- package/dist/utils/detect-country.js +6 -0
- package/dist/utils/func.d.ts +61 -0
- package/dist/utils/func.js +24 -0
- package/dist/utils/rate-limit.js +10 -0
- package/package.json +16 -3
- package/dist/checkout.mjs +0 -1
- package/dist/chunk-6GPWNCDO.mjs +0 -130
- package/dist/chunk-EJO5U4BT.mjs +0 -2
- package/dist/chunk-FFKWCNLU.mjs +0 -1
- package/dist/chunk-KYLPIEU3.mjs +0 -2
- package/dist/chunk-MB2INNNP.mjs +0 -1
- package/dist/chunk-MI7754VX.mjs +0 -2
- package/dist/chunk-SZQPMLZG.mjs +0 -1
- package/dist/collections.mjs +0 -1
- package/dist/enrich-OZHBXKK6.mjs +0 -1
- package/dist/index.mjs +0 -2
- package/dist/products.mjs +0 -1
- package/dist/store.mjs +0 -1
- package/dist/utils/rate-limit.mjs +0 -1
package/README.md
CHANGED
|
@@ -622,6 +622,45 @@ Notes:
|
|
|
622
622
|
- npm publishes use OIDC with provenance; no `NPM_TOKEN` secret is required.
|
|
623
623
|
- Ensure your npm package settings add this GitHub repo as a trusted publisher and set the environment name to `npm-publish`.
|
|
624
624
|
|
|
625
|
+
### Additional Utilities
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
import {
|
|
629
|
+
calculateDiscount,
|
|
630
|
+
extractDomainWithoutSuffix,
|
|
631
|
+
generateStoreSlug,
|
|
632
|
+
genProductSlug,
|
|
633
|
+
detectShopCountry,
|
|
634
|
+
} from 'shop-client';
|
|
635
|
+
|
|
636
|
+
// Discount calculation (percentage, rounded to nearest integer)
|
|
637
|
+
calculateDiscount(8000, 10000); // 20
|
|
638
|
+
|
|
639
|
+
// Extract base domain without public suffix
|
|
640
|
+
extractDomainWithoutSuffix('www.example.co.uk'); // 'example'
|
|
641
|
+
|
|
642
|
+
// Create an SEO-friendly store slug
|
|
643
|
+
generateStoreSlug('https://shop.example.com'); // 'shop-example-com'
|
|
644
|
+
|
|
645
|
+
// Create a product slug from product data
|
|
646
|
+
genProductSlug({
|
|
647
|
+
title: 'Summer Dress',
|
|
648
|
+
handle: 'summer-dress',
|
|
649
|
+
vendor: 'Acme',
|
|
650
|
+
}); // 'acme-summer-dress'
|
|
651
|
+
|
|
652
|
+
// Detect Shopify store country with confidence score
|
|
653
|
+
const result = await detectShopCountry('anuki.in');
|
|
654
|
+
// result.country → 'IN', result.confidence → 0.9
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Notes:
|
|
658
|
+
- `calculateDiscount` expects prices in the same unit (e.g., cents) and returns an integer percentage.
|
|
659
|
+
- `extractDomainWithoutSuffix` removes known TLDs/suffixes, leaving the registrable label.
|
|
660
|
+
- `generateStoreSlug` preserves domain components and replaces separators with hyphens.
|
|
661
|
+
- `genProductSlug` builds a stable, vendor-prefixed slug using product fields.
|
|
662
|
+
- `detectShopCountry` combines multiple signals to infer store country and confidence.
|
|
663
|
+
|
|
625
664
|
### Store Type Classification
|
|
626
665
|
|
|
627
666
|
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.
|
|
@@ -657,6 +696,32 @@ Details:
|
|
|
657
696
|
- If `OPENROUTER_API_KEY` is absent or `OPENROUTER_OFFLINE=1`, uses offline regex heuristics.
|
|
658
697
|
- Applies store-level pruning based on title/description to improve consistency.
|
|
659
698
|
|
|
699
|
+
### AI Enrichment
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
import { classifyProduct, generateSEOContent } from 'shop-client';
|
|
703
|
+
|
|
704
|
+
// Classify a single product (offline or via OpenRouter when configured)
|
|
705
|
+
const classification = await classifyProduct({
|
|
706
|
+
title: 'Organic Cotton T-Shirt',
|
|
707
|
+
bodyHtml: '<p>Soft, breathable cotton tee</p>',
|
|
708
|
+
tags: ['organic', 'cotton', 'unisex'],
|
|
709
|
+
});
|
|
710
|
+
// classification → { adult_unisex: { clothing: ['t-shirts'] } }
|
|
711
|
+
|
|
712
|
+
// Generate basic SEO content for a product
|
|
713
|
+
const seo = await generateSEOContent({
|
|
714
|
+
title: 'Organic Cotton T-Shirt',
|
|
715
|
+
description: 'Soft, breathable tee for everyday wear',
|
|
716
|
+
tags: ['organic', 'cotton', 'unisex'],
|
|
717
|
+
});
|
|
718
|
+
// seo.metaTitle, seo.metaDescription, seo.keywords
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
Notes:
|
|
722
|
+
- `classifyProduct` mirrors the store-level classification logic but operates on a single product.
|
|
723
|
+
- `generateSEOContent` produces lightweight, deterministic metadata suitable for catalogs and PDPs.
|
|
724
|
+
|
|
660
725
|
## 🏗️ Type Definitions
|
|
661
726
|
|
|
662
727
|
### StoreInfo
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { k as ProductClassification, l as SEOContent, a as ShopifySingleProduct } from '../types-luPg5O08.js';
|
|
2
|
+
|
|
3
|
+
interface EnrichedProductResult {
|
|
4
|
+
bodyHtml: string;
|
|
5
|
+
pageHtml: string;
|
|
6
|
+
extractedMainHtml: string;
|
|
7
|
+
mergedMarkdown: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Fetch Shopify Product AJAX API
|
|
11
|
+
* /products/{handle}.js
|
|
12
|
+
*/
|
|
13
|
+
declare function fetchAjaxProduct(domain: string, handle: string): Promise<ShopifySingleProduct>;
|
|
14
|
+
/**
|
|
15
|
+
* Fetch full product page HTML
|
|
16
|
+
*/
|
|
17
|
+
declare function fetchProductPage(domain: string, handle: string): Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Extract the main Shopify product section WITHOUT cheerio
|
|
20
|
+
* Uses regex + indexing (fast & reliable)
|
|
21
|
+
*/
|
|
22
|
+
declare function extractMainSection(html: string): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Convert HTML → Clean Markdown using Turndown
|
|
25
|
+
* Includes Shopify cleanup rules + GFM support
|
|
26
|
+
*/
|
|
27
|
+
declare function htmlToMarkdown(html: string | null, options?: {
|
|
28
|
+
useGfm?: boolean;
|
|
29
|
+
}): string;
|
|
30
|
+
/**
|
|
31
|
+
* Merge the two markdown sources using OpenAI GPT
|
|
32
|
+
*/
|
|
33
|
+
declare function mergeWithLLM(bodyInput: string, pageInput: string, options?: {
|
|
34
|
+
apiKey?: string;
|
|
35
|
+
inputType?: "markdown" | "html";
|
|
36
|
+
model?: string;
|
|
37
|
+
outputFormat?: "markdown" | "json";
|
|
38
|
+
}): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* MAIN WORKFLOW
|
|
41
|
+
*/
|
|
42
|
+
declare function enrichProduct(domain: string, handle: string, options?: {
|
|
43
|
+
apiKey?: string;
|
|
44
|
+
useGfm?: boolean;
|
|
45
|
+
inputType?: "markdown" | "html";
|
|
46
|
+
model?: string;
|
|
47
|
+
outputFormat?: "markdown" | "json";
|
|
48
|
+
}): Promise<EnrichedProductResult>;
|
|
49
|
+
/**
|
|
50
|
+
* Classify product content into a three-tier hierarchy using LLM.
|
|
51
|
+
* Returns strictly validated JSON with audience, vertical, and optional category/subCategory.
|
|
52
|
+
*/
|
|
53
|
+
declare function classifyProduct(productContent: string, options?: {
|
|
54
|
+
apiKey?: string;
|
|
55
|
+
model?: string;
|
|
56
|
+
}): Promise<ProductClassification>;
|
|
57
|
+
/**
|
|
58
|
+
* Generate SEO and marketing content for a product. Returns strictly validated JSON.
|
|
59
|
+
*/
|
|
60
|
+
declare function generateSEOContent(product: {
|
|
61
|
+
title: string;
|
|
62
|
+
description?: string;
|
|
63
|
+
vendor?: string;
|
|
64
|
+
price?: number;
|
|
65
|
+
tags?: string[];
|
|
66
|
+
}, options?: {
|
|
67
|
+
apiKey?: string;
|
|
68
|
+
model?: string;
|
|
69
|
+
}): Promise<SEOContent>;
|
|
70
|
+
/**
|
|
71
|
+
* Determine store type (primary vertical and audience) from store information.
|
|
72
|
+
* Accepts flexible input for showcase products/collections (titles or handles) and returns
|
|
73
|
+
* strictly validated `vertical` and `audience` values.
|
|
74
|
+
*/
|
|
75
|
+
declare function determineStoreType(storeInfo: {
|
|
76
|
+
title: string;
|
|
77
|
+
description?: string | null;
|
|
78
|
+
showcase: {
|
|
79
|
+
products: Array<{
|
|
80
|
+
title: string;
|
|
81
|
+
productType?: string | null;
|
|
82
|
+
}> | string[];
|
|
83
|
+
collections: Array<{
|
|
84
|
+
title: string;
|
|
85
|
+
}> | string[];
|
|
86
|
+
};
|
|
87
|
+
}, options?: {
|
|
88
|
+
apiKey?: string;
|
|
89
|
+
model?: string;
|
|
90
|
+
}): Promise<Partial<Record<ProductClassification["audience"], Partial<Record<ProductClassification["vertical"], string[]>>>>>;
|
|
91
|
+
declare function pruneBreakdownForSignals(breakdown: Partial<Record<ProductClassification["audience"], Partial<Record<ProductClassification["vertical"], string[]>>>>, text: string): Partial<Record<ProductClassification["audience"], Partial<Record<ProductClassification["vertical"], string[]>>>>;
|
|
92
|
+
|
|
93
|
+
export { type EnrichedProductResult, classifyProduct, determineStoreType, enrichProduct, extractMainSection, fetchAjaxProduct, fetchProductPage, generateSEOContent, htmlToMarkdown, mergeWithLLM, pruneBreakdownForSignals };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
classifyProduct,
|
|
3
|
+
determineStoreType,
|
|
4
|
+
enrichProduct,
|
|
5
|
+
extractMainSection,
|
|
6
|
+
fetchAjaxProduct,
|
|
7
|
+
fetchProductPage,
|
|
8
|
+
generateSEOContent,
|
|
9
|
+
htmlToMarkdown,
|
|
10
|
+
mergeWithLLM,
|
|
11
|
+
pruneBreakdownForSignals
|
|
12
|
+
} from "../chunk-VPPCOJC3.js";
|
|
13
|
+
import "../chunk-2MF53V33.js";
|
|
14
|
+
export {
|
|
15
|
+
classifyProduct,
|
|
16
|
+
determineStoreType,
|
|
17
|
+
enrichProduct,
|
|
18
|
+
extractMainSection,
|
|
19
|
+
fetchAjaxProduct,
|
|
20
|
+
fetchProductPage,
|
|
21
|
+
generateSEOContent,
|
|
22
|
+
htmlToMarkdown,
|
|
23
|
+
mergeWithLLM,
|
|
24
|
+
pruneBreakdownForSignals
|
|
25
|
+
};
|
package/dist/checkout.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// src/utils/rate-limit.ts
|
|
2
|
+
var RateLimiter = class {
|
|
3
|
+
constructor(options) {
|
|
4
|
+
this.queue = [];
|
|
5
|
+
this.inFlight = 0;
|
|
6
|
+
this.refillTimer = null;
|
|
7
|
+
this.options = options;
|
|
8
|
+
this.tokens = options.maxRequestsPerInterval;
|
|
9
|
+
}
|
|
10
|
+
startRefill() {
|
|
11
|
+
if (this.refillTimer) return;
|
|
12
|
+
this.refillTimer = setInterval(() => {
|
|
13
|
+
this.tokens = this.options.maxRequestsPerInterval;
|
|
14
|
+
this.tryRun();
|
|
15
|
+
}, this.options.intervalMs);
|
|
16
|
+
if (this.refillTimer && typeof this.refillTimer.unref === "function") {
|
|
17
|
+
this.refillTimer.unref();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
ensureRefillStarted() {
|
|
21
|
+
if (!this.refillTimer) {
|
|
22
|
+
this.startRefill();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
configure(next) {
|
|
26
|
+
this.options = { ...this.options, ...next };
|
|
27
|
+
this.options.maxRequestsPerInterval = Math.max(
|
|
28
|
+
1,
|
|
29
|
+
this.options.maxRequestsPerInterval
|
|
30
|
+
);
|
|
31
|
+
this.options.intervalMs = Math.max(10, this.options.intervalMs);
|
|
32
|
+
this.options.maxConcurrency = Math.max(1, this.options.maxConcurrency);
|
|
33
|
+
}
|
|
34
|
+
schedule(fn) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
this.ensureRefillStarted();
|
|
37
|
+
this.queue.push({ fn, resolve, reject });
|
|
38
|
+
this.tryRun();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
tryRun() {
|
|
42
|
+
while (this.queue.length > 0 && this.inFlight < this.options.maxConcurrency && this.tokens > 0) {
|
|
43
|
+
const task = this.queue.shift();
|
|
44
|
+
this.tokens -= 1;
|
|
45
|
+
this.inFlight += 1;
|
|
46
|
+
Promise.resolve().then(task.fn).then((result) => task.resolve(result)).catch((err) => task.reject(err)).finally(() => {
|
|
47
|
+
this.inFlight -= 1;
|
|
48
|
+
setTimeout(() => this.tryRun(), 0);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var enabled = false;
|
|
54
|
+
var defaultOptions = {
|
|
55
|
+
maxRequestsPerInterval: 5,
|
|
56
|
+
// 5 requests
|
|
57
|
+
intervalMs: 1e3,
|
|
58
|
+
// per second
|
|
59
|
+
maxConcurrency: 5
|
|
60
|
+
// up to 5 in parallel
|
|
61
|
+
};
|
|
62
|
+
var limiter = new RateLimiter(defaultOptions);
|
|
63
|
+
var hostLimiters = /* @__PURE__ */ new Map();
|
|
64
|
+
var classLimiters = /* @__PURE__ */ new Map();
|
|
65
|
+
function getHost(input) {
|
|
66
|
+
try {
|
|
67
|
+
if (typeof input === "string") {
|
|
68
|
+
return new URL(input).host;
|
|
69
|
+
}
|
|
70
|
+
if (input instanceof URL) {
|
|
71
|
+
return input.host;
|
|
72
|
+
}
|
|
73
|
+
const url = input.url;
|
|
74
|
+
if (url) {
|
|
75
|
+
return new URL(url).host;
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
function getHostLimiter(host) {
|
|
82
|
+
if (!host) return void 0;
|
|
83
|
+
const exact = hostLimiters.get(host);
|
|
84
|
+
if (exact) return exact;
|
|
85
|
+
for (const [key, lim] of hostLimiters.entries()) {
|
|
86
|
+
if (key.startsWith("*.") && host.endsWith(key.slice(2))) {
|
|
87
|
+
return lim;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
function configureRateLimit(options) {
|
|
93
|
+
var _a, _b, _c, _d, _e, _f;
|
|
94
|
+
if (typeof options.enabled === "boolean") {
|
|
95
|
+
enabled = options.enabled;
|
|
96
|
+
}
|
|
97
|
+
const { perHost, perClass } = options;
|
|
98
|
+
const globalOpts = {};
|
|
99
|
+
if (typeof options.maxRequestsPerInterval === "number") {
|
|
100
|
+
globalOpts.maxRequestsPerInterval = options.maxRequestsPerInterval;
|
|
101
|
+
}
|
|
102
|
+
if (typeof options.intervalMs === "number") {
|
|
103
|
+
globalOpts.intervalMs = options.intervalMs;
|
|
104
|
+
}
|
|
105
|
+
if (typeof options.maxConcurrency === "number") {
|
|
106
|
+
globalOpts.maxConcurrency = options.maxConcurrency;
|
|
107
|
+
}
|
|
108
|
+
if (Object.keys(globalOpts).length) {
|
|
109
|
+
limiter.configure(globalOpts);
|
|
110
|
+
}
|
|
111
|
+
if (perHost) {
|
|
112
|
+
for (const host of Object.keys(perHost)) {
|
|
113
|
+
const opts = perHost[host];
|
|
114
|
+
const existing = hostLimiters.get(host);
|
|
115
|
+
if (existing) {
|
|
116
|
+
existing.configure(opts);
|
|
117
|
+
} else {
|
|
118
|
+
hostLimiters.set(
|
|
119
|
+
host,
|
|
120
|
+
new RateLimiter({
|
|
121
|
+
maxRequestsPerInterval: (_a = opts.maxRequestsPerInterval) != null ? _a : defaultOptions.maxRequestsPerInterval,
|
|
122
|
+
intervalMs: (_b = opts.intervalMs) != null ? _b : defaultOptions.intervalMs,
|
|
123
|
+
maxConcurrency: (_c = opts.maxConcurrency) != null ? _c : defaultOptions.maxConcurrency
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (perClass) {
|
|
130
|
+
for (const klass of Object.keys(perClass)) {
|
|
131
|
+
const opts = perClass[klass];
|
|
132
|
+
const existing = classLimiters.get(klass);
|
|
133
|
+
if (existing) {
|
|
134
|
+
existing.configure(opts);
|
|
135
|
+
} else {
|
|
136
|
+
classLimiters.set(
|
|
137
|
+
klass,
|
|
138
|
+
new RateLimiter({
|
|
139
|
+
maxRequestsPerInterval: (_d = opts.maxRequestsPerInterval) != null ? _d : defaultOptions.maxRequestsPerInterval,
|
|
140
|
+
intervalMs: (_e = opts.intervalMs) != null ? _e : defaultOptions.intervalMs,
|
|
141
|
+
maxConcurrency: (_f = opts.maxConcurrency) != null ? _f : defaultOptions.maxConcurrency
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function sleep(ms) {
|
|
149
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
150
|
+
}
|
|
151
|
+
async function rateLimitedFetch(input, init) {
|
|
152
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
153
|
+
const klass = init == null ? void 0 : init.rateLimitClass;
|
|
154
|
+
const byClass = klass ? classLimiters.get(klass) : void 0;
|
|
155
|
+
const byHost = getHostLimiter(getHost(input));
|
|
156
|
+
const eff = enabled ? (_a = byClass != null ? byClass : byHost) != null ? _a : limiter : void 0;
|
|
157
|
+
const maxRetries = Math.max(0, (_c = (_b = init == null ? void 0 : init.retry) == null ? void 0 : _b.maxRetries) != null ? _c : 2);
|
|
158
|
+
const baseDelayMs = Math.max(0, (_e = (_d = init == null ? void 0 : init.retry) == null ? void 0 : _d.baseDelayMs) != null ? _e : 200);
|
|
159
|
+
const retryOnStatuses = (_g = (_f = init == null ? void 0 : init.retry) == null ? void 0 : _f.retryOnStatuses) != null ? _g : [429, 503];
|
|
160
|
+
let attempt = 0;
|
|
161
|
+
let lastError = null;
|
|
162
|
+
let response = null;
|
|
163
|
+
while (attempt <= maxRetries) {
|
|
164
|
+
try {
|
|
165
|
+
if (eff) {
|
|
166
|
+
response = await eff.schedule(() => fetch(input, init));
|
|
167
|
+
} else {
|
|
168
|
+
response = await fetch(input, init);
|
|
169
|
+
}
|
|
170
|
+
if (!response || response.ok || !retryOnStatuses.includes(response.status)) {
|
|
171
|
+
return response;
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
lastError = err;
|
|
175
|
+
}
|
|
176
|
+
attempt += 1;
|
|
177
|
+
if (attempt > maxRetries) break;
|
|
178
|
+
const jitter = Math.floor(Math.random() * 100);
|
|
179
|
+
const delay = baseDelayMs * 2 ** (attempt - 1) + jitter;
|
|
180
|
+
await sleep(delay);
|
|
181
|
+
}
|
|
182
|
+
if (response) return response;
|
|
183
|
+
throw lastError != null ? lastError : new Error("rateLimitedFetch failed without response");
|
|
184
|
+
}
|
|
185
|
+
function getRateLimitStatus() {
|
|
186
|
+
return {
|
|
187
|
+
enabled,
|
|
188
|
+
options: { ...defaultOptions }
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export {
|
|
193
|
+
configureRateLimit,
|
|
194
|
+
rateLimitedFetch,
|
|
195
|
+
getRateLimitStatus
|
|
196
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// src/utils/func.ts
|
|
2
|
+
import { parse } from "tldts";
|
|
3
|
+
function extractDomainWithoutSuffix(domain) {
|
|
4
|
+
const parsedDomain = parse(domain);
|
|
5
|
+
return parsedDomain.domainWithoutSuffix;
|
|
6
|
+
}
|
|
7
|
+
function generateStoreSlug(domain) {
|
|
8
|
+
var _a;
|
|
9
|
+
const input = new URL(domain);
|
|
10
|
+
const parsedDomain = parse(input.href);
|
|
11
|
+
const domainName = (_a = parsedDomain.domainWithoutSuffix) != null ? _a : input.hostname.split(".")[0];
|
|
12
|
+
return (domainName || "").toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
13
|
+
}
|
|
14
|
+
var genProductSlug = ({
|
|
15
|
+
handle,
|
|
16
|
+
storeDomain
|
|
17
|
+
}) => {
|
|
18
|
+
const storeSlug = generateStoreSlug(storeDomain);
|
|
19
|
+
return `${handle}-by-${storeSlug}`;
|
|
20
|
+
};
|
|
21
|
+
var calculateDiscount = (price, compareAtPrice) => !compareAtPrice || compareAtPrice === 0 ? 0 : Math.max(
|
|
22
|
+
0,
|
|
23
|
+
Math.round(100 - price / compareAtPrice * 100)
|
|
24
|
+
// Removed the decimal precision
|
|
25
|
+
);
|
|
26
|
+
function sanitizeDomain(input, opts) {
|
|
27
|
+
var _a;
|
|
28
|
+
if (typeof input !== "string") {
|
|
29
|
+
throw new Error("sanitizeDomain: input must be a string");
|
|
30
|
+
}
|
|
31
|
+
let raw = input.trim();
|
|
32
|
+
if (!raw) {
|
|
33
|
+
throw new Error("sanitizeDomain: input cannot be empty");
|
|
34
|
+
}
|
|
35
|
+
const hasProtocol = /^[a-z]+:\/\//i.test(raw);
|
|
36
|
+
if (!hasProtocol && !raw.startsWith("//")) {
|
|
37
|
+
raw = `https://${raw}`;
|
|
38
|
+
}
|
|
39
|
+
const stripWWW = (_a = opts == null ? void 0 : opts.stripWWW) != null ? _a : true;
|
|
40
|
+
try {
|
|
41
|
+
let url;
|
|
42
|
+
if (raw.startsWith("//")) {
|
|
43
|
+
url = new URL(`https:${raw}`);
|
|
44
|
+
} else if (raw.includes("://")) {
|
|
45
|
+
url = new URL(raw);
|
|
46
|
+
} else {
|
|
47
|
+
url = new URL(`https://${raw}`);
|
|
48
|
+
}
|
|
49
|
+
let hostname = url.hostname.toLowerCase();
|
|
50
|
+
const hadWWW = /^www\./i.test(url.hostname);
|
|
51
|
+
if (stripWWW) hostname = hostname.replace(/^www\./, "");
|
|
52
|
+
if (!hostname.includes(".")) {
|
|
53
|
+
throw new Error("sanitizeDomain: invalid domain (missing suffix)");
|
|
54
|
+
}
|
|
55
|
+
const parsed = parse(hostname);
|
|
56
|
+
if (!parsed.publicSuffix || parsed.isIcann === false) {
|
|
57
|
+
throw new Error("sanitizeDomain: invalid domain (missing suffix)");
|
|
58
|
+
}
|
|
59
|
+
if (!stripWWW && hadWWW) {
|
|
60
|
+
return `www.${parsed.domain || hostname}`;
|
|
61
|
+
}
|
|
62
|
+
return parsed.domain || hostname;
|
|
63
|
+
} catch {
|
|
64
|
+
let hostname = raw.toLowerCase();
|
|
65
|
+
hostname = hostname.replace(/^[a-z]+:\/\//, "");
|
|
66
|
+
hostname = hostname.replace(/^\/\//, "");
|
|
67
|
+
hostname = hostname.replace(/[/:#?].*$/, "");
|
|
68
|
+
const hadWWW = /^www\./i.test(hostname);
|
|
69
|
+
if (stripWWW) hostname = hostname.replace(/^www\./, "");
|
|
70
|
+
if (!hostname.includes(".")) {
|
|
71
|
+
throw new Error("sanitizeDomain: invalid domain (missing suffix)");
|
|
72
|
+
}
|
|
73
|
+
const parsed = parse(hostname);
|
|
74
|
+
if (!parsed.publicSuffix || parsed.isIcann === false) {
|
|
75
|
+
throw new Error("sanitizeDomain: invalid domain (missing suffix)");
|
|
76
|
+
}
|
|
77
|
+
if (!stripWWW && hadWWW) {
|
|
78
|
+
return `www.${parsed.domain || hostname}`;
|
|
79
|
+
}
|
|
80
|
+
return parsed.domain || hostname;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function safeParseDate(input) {
|
|
84
|
+
if (!input || typeof input !== "string") return void 0;
|
|
85
|
+
const d = new Date(input);
|
|
86
|
+
return Number.isNaN(d.getTime()) ? void 0 : d;
|
|
87
|
+
}
|
|
88
|
+
function normalizeKey(input) {
|
|
89
|
+
return input.toLowerCase().replace(/\s+/g, "_");
|
|
90
|
+
}
|
|
91
|
+
function buildVariantOptionsMap(optionNames, variants) {
|
|
92
|
+
const keys = optionNames.map(normalizeKey);
|
|
93
|
+
const map = {};
|
|
94
|
+
for (const v of variants) {
|
|
95
|
+
const parts = [];
|
|
96
|
+
if (keys[0] && v.option1)
|
|
97
|
+
parts.push(`${keys[0]}#${normalizeKey(v.option1)}`);
|
|
98
|
+
if (keys[1] && v.option2)
|
|
99
|
+
parts.push(`${keys[1]}#${normalizeKey(v.option2)}`);
|
|
100
|
+
if (keys[2] && v.option3)
|
|
101
|
+
parts.push(`${keys[2]}#${normalizeKey(v.option3)}`);
|
|
102
|
+
if (parts.length > 0) {
|
|
103
|
+
if (parts.length > 1) parts.sort();
|
|
104
|
+
const key = parts.join("##");
|
|
105
|
+
const id = v.id.toString();
|
|
106
|
+
if (map[key] === void 0) {
|
|
107
|
+
map[key] = id;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return map;
|
|
112
|
+
}
|
|
113
|
+
function buildVariantKey(obj) {
|
|
114
|
+
const parts = [];
|
|
115
|
+
for (const [name, value] of Object.entries(obj)) {
|
|
116
|
+
if (value) {
|
|
117
|
+
parts.push(`${normalizeKey(name)}#${normalizeKey(value)}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (parts.length === 0) return "";
|
|
121
|
+
parts.sort((a, b) => a.localeCompare(b));
|
|
122
|
+
return parts.join("##");
|
|
123
|
+
}
|
|
124
|
+
function formatPrice(amountInCents, currency) {
|
|
125
|
+
try {
|
|
126
|
+
return new Intl.NumberFormat(void 0, {
|
|
127
|
+
style: "currency",
|
|
128
|
+
currency
|
|
129
|
+
}).format((amountInCents || 0) / 100);
|
|
130
|
+
} catch {
|
|
131
|
+
const val = (amountInCents || 0) / 100;
|
|
132
|
+
return `${val} ${currency}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export {
|
|
137
|
+
extractDomainWithoutSuffix,
|
|
138
|
+
generateStoreSlug,
|
|
139
|
+
genProductSlug,
|
|
140
|
+
calculateDiscount,
|
|
141
|
+
sanitizeDomain,
|
|
142
|
+
safeParseDate,
|
|
143
|
+
normalizeKey,
|
|
144
|
+
buildVariantOptionsMap,
|
|
145
|
+
buildVariantKey,
|
|
146
|
+
formatPrice
|
|
147
|
+
};
|