@reactionary/feeds 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +257 -0
  2. package/bin/reactionary-feeds +2 -0
  3. package/cli.cjs +170941 -0
  4. package/cli.js +340 -0
  5. package/documentation/basic-product-feeds.config.mjs +32 -0
  6. package/documentation/multilingual-feeds.config.mjs +59 -0
  7. package/documentation/nextjs-route-example.mjs +31 -0
  8. package/documentation/sitemaps.config.mjs +77 -0
  9. package/index.js +13 -0
  10. package/lib/default-registry.js +18 -0
  11. package/lib/env-client-builder.js +244 -0
  12. package/lib/feed-generator.js +395 -0
  13. package/lib/feed-server.js +305 -0
  14. package/lib/feed-transformer.js +21 -0
  15. package/lib/feed-types.js +45 -0
  16. package/lib/load-cwd-env.js +16 -0
  17. package/lib/sitemap-generator.js +173 -0
  18. package/lib/sitemap-xml.js +32 -0
  19. package/lib/transformers/acp-product-feed.transformer.js +128 -0
  20. package/lib/transformers/google-merchant-feed.transformer.js +60 -0
  21. package/lib/transformers/pricerunner-feed.transformer.js +60 -0
  22. package/lib/transformers/shared.js +25 -0
  23. package/lib/transformers/sitemap-feed.transformer.js +26 -0
  24. package/package.json +32 -0
  25. package/src/cli.d.ts +2 -0
  26. package/src/index.d.ts +13 -0
  27. package/src/lib/default-registry.d.ts +2 -0
  28. package/src/lib/env-client-builder.d.ts +18 -0
  29. package/src/lib/feed-generator.d.ts +17 -0
  30. package/src/lib/feed-server.d.ts +28 -0
  31. package/src/lib/feed-transformer.d.ts +30 -0
  32. package/src/lib/feed-types.d.ts +185 -0
  33. package/src/lib/load-cwd-env.d.ts +4 -0
  34. package/src/lib/sitemap-generator.d.ts +20 -0
  35. package/src/lib/sitemap-xml.d.ts +3 -0
  36. package/src/lib/transformers/acp-product-feed.transformer.d.ts +4 -0
  37. package/src/lib/transformers/google-merchant-feed.transformer.d.ts +2 -0
  38. package/src/lib/transformers/pricerunner-feed.transformer.d.ts +2 -0
  39. package/src/lib/transformers/shared.d.ts +6 -0
  40. package/src/lib/transformers/sitemap-feed.transformer.d.ts +2 -0
@@ -0,0 +1,244 @@
1
+ import { withAlgoliaCapabilities } from "@reactionary/algolia";
2
+ import {
3
+ withCommercetoolsCapabilities
4
+ } from "@reactionary/commercetools";
5
+ import {
6
+ ClientBuilder,
7
+ MemoryCache,
8
+ createInitialRequestContext
9
+ } from "@reactionary/core";
10
+ import { withFakeCapabilities } from "@reactionary/fake";
11
+ import { withMagentoCapabilities } from "@reactionary/magento";
12
+ import { withMedusaCapabilities } from "@reactionary/medusa";
13
+ import {
14
+ withMeilisearchCapabilities
15
+ } from "@reactionary/meilisearch";
16
+ import { loadCwdEnv } from "./load-cwd-env.js";
17
+ const providerSystems = [
18
+ "FAKE",
19
+ "MAGENTO",
20
+ "MEDUSA",
21
+ "COMMERCETOOLS",
22
+ "ALGOLIA",
23
+ "MEILISEARCH",
24
+ "UNOMI"
25
+ ];
26
+ function createReactionaryFeedClientFromEnv(options = {}) {
27
+ if (options.loadEnv !== false) {
28
+ loadCwdEnv({
29
+ env: options.env
30
+ });
31
+ }
32
+ const env = options.env ?? process.env;
33
+ const enabledSystems = getEnabledReactionaryFeedProviderSystems(env);
34
+ if (enabledSystems.length === 0) {
35
+ throw new Error(getNoEnabledProviderSystemsMessage());
36
+ }
37
+ const context = {
38
+ ...createInitialRequestContext(),
39
+ ...options.contextOverrides
40
+ };
41
+ let builder = new ClientBuilder(context).withCache(
42
+ options.cache ?? new MemoryCache()
43
+ );
44
+ for (const system of enabledSystems) {
45
+ switch (system) {
46
+ case "ALGOLIA":
47
+ builder = builder.withCapability(
48
+ withAlgoliaCapabilities(getAlgoliaConfiguration(env), {
49
+ productSearch: { enabled: true }
50
+ })
51
+ );
52
+ break;
53
+ case "COMMERCETOOLS":
54
+ builder = builder.withCapability(
55
+ withCommercetoolsCapabilities(getCommercetoolsConfiguration(env), {
56
+ product: { enabled: true },
57
+ category: { enabled: true },
58
+ inventory: { enabled: true },
59
+ price: { enabled: true },
60
+ productSearch: { enabled: true },
61
+ productReviews: { enabled: true },
62
+ store: { enabled: true }
63
+ })
64
+ );
65
+ break;
66
+ case "FAKE":
67
+ builder = builder.withCapability(
68
+ withFakeCapabilities(getFakeConfiguration(), {
69
+ productSearch: { enabled: true },
70
+ category: { enabled: true },
71
+ store: { enabled: true },
72
+ price: { enabled: true },
73
+ inventory: { enabled: true },
74
+ product: { enabled: true },
75
+ productReviews: { enabled: true }
76
+ })
77
+ );
78
+ break;
79
+ case "MAGENTO":
80
+ builder = builder.withCapability(
81
+ withMagentoCapabilities(getMagentoConfiguration(env), {
82
+ product: { enabled: true },
83
+ category: { enabled: true },
84
+ inventory: { enabled: true },
85
+ price: { enabled: true },
86
+ productSearch: { enabled: true },
87
+ productReviews: { enabled: true }
88
+ })
89
+ );
90
+ break;
91
+ case "MEDUSA":
92
+ builder = builder.withCapability(
93
+ withMedusaCapabilities(getMedusaConfiguration(env), {
94
+ product: { enabled: true },
95
+ category: { enabled: true },
96
+ inventory: { enabled: true },
97
+ price: { enabled: true },
98
+ productSearch: { enabled: true }
99
+ })
100
+ );
101
+ break;
102
+ case "MEILISEARCH":
103
+ builder = builder.withCapability(
104
+ withMeilisearchCapabilities(getMeilisearchConfiguration(env), {
105
+ productSearch: { enabled: true }
106
+ })
107
+ );
108
+ break;
109
+ case "UNOMI":
110
+ break;
111
+ }
112
+ }
113
+ return {
114
+ client: builder.build(),
115
+ enabledSystems
116
+ };
117
+ }
118
+ function createReactionaryFeedClientFactoryFromEnv(options = {}) {
119
+ return (requestContext) => createReactionaryFeedClientFromEnv({
120
+ ...options,
121
+ contextOverrides: requestContext
122
+ }).client;
123
+ }
124
+ function getEnabledReactionaryFeedProviderSystems(env = process.env) {
125
+ return providerSystems.filter(
126
+ (system) => isEnvEnabled(env[`ENABLED_${system}`])
127
+ );
128
+ }
129
+ function getNoEnabledProviderSystemsMessage() {
130
+ return `No Reactionary provider system is enabled. Set one of ${providerSystems.map((system) => `ENABLED_${system}=true`).join(", ")}.`;
131
+ }
132
+ function parseCommaSeparatedEnvList(value) {
133
+ return (value || "").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
134
+ }
135
+ function isEnvEnabled(value) {
136
+ return value?.toLowerCase() === "true";
137
+ }
138
+ function getAlgoliaConfiguration(env) {
139
+ return {
140
+ apiKey: env["ALGOLIA_API_KEY"] || "",
141
+ appId: env["ALGOLIA_APP_ID"] || "",
142
+ indexName: env["ALGOLIA_INDEX"] || "",
143
+ useRecommendationsForBots: env["ALGOLIA_USE_RECOMMENDATIONS_FOR_BOTS"] === "true",
144
+ useBaseIndexNameForEnglishLocale: env["ALGOLIA_USE_BASE_INDEX_NAME_FOR_ENGLISH_LOCALE"] === "true"
145
+ };
146
+ }
147
+ function getCommercetoolsConfiguration(env) {
148
+ return {
149
+ apiUrl: env["CTP_API_URL"] || "",
150
+ authUrl: env["CTP_AUTH_URL"] || "",
151
+ clientId: env["CTP_CLIENT_ID"] || "",
152
+ clientSecret: env["CTP_CLIENT_SECRET"] || "",
153
+ projectKey: env["CTP_PROJECT_KEY"] || "",
154
+ scopes: parseCommaSeparatedEnvList(env["CTP_SCOPES"]),
155
+ paymentMethods: [
156
+ {
157
+ identifier: {
158
+ method: "stripe",
159
+ name: "Stripe",
160
+ paymentProcessor: "stripe"
161
+ },
162
+ isPunchOut: false,
163
+ description: "Stripe payment gateway"
164
+ }
165
+ ],
166
+ facetFieldsForSearch: parseCommaSeparatedEnvList(
167
+ env["CTP_FACET_FIELDS_FOR_SEARCH"]
168
+ ),
169
+ adminClientId: env["CTP_ADMIN_CLIENT_ID"] || void 0,
170
+ adminClientSecret: env["CTP_ADMIN_CLIENT_SECRET"] || void 0,
171
+ listPriceChannelKey: env["CTP_LIST_PRICE_CHANNEL_KEY"] || void 0,
172
+ customerPriceChannelKey: env["CTP_CUSTOMER_PRICE_CHANNEL_KEY"] || void 0
173
+ };
174
+ }
175
+ function getFakeConfiguration() {
176
+ return {
177
+ jitter: {
178
+ mean: 0,
179
+ deviation: 0
180
+ },
181
+ seeds: {
182
+ product: 1,
183
+ search: 1,
184
+ category: 1
185
+ },
186
+ featureFlags: {
187
+ flags: [
188
+ {
189
+ key: "true-flag",
190
+ type: "boolean",
191
+ enabled: true
192
+ },
193
+ {
194
+ key: "string-flag",
195
+ type: "multivariate",
196
+ variants: ["red", "green", "blue"],
197
+ enabledVariant: "blue"
198
+ }
199
+ ]
200
+ }
201
+ };
202
+ }
203
+ function getMagentoConfiguration(env) {
204
+ return {
205
+ adminApiKey: env["MAGENTO_ADMIN_API_KEY"] || "",
206
+ baseUrl: env["MAGENTO_BASE_URL"] || "",
207
+ storeCode: env["MAGENTO_STORE_CODE"] || "",
208
+ authStoreCode: env["MAGENTO_AUTH_STORE_CODE"] || "default",
209
+ mediaUrl: env["MAGENTO_MEDIA_URL"] || void 0,
210
+ mediaSource: env["MAGENTO_MEDIA_SOURCE"] === "EXTERNAL" ? "EXTERNAL" : "DEFAULT",
211
+ defaultCurrency: env["MAGENTO_DEFAULT_CURRENCY"] || "",
212
+ rootCategoryId: env["MAGENTO_ROOT_CATEGORY_ID"] || "2",
213
+ graphqlUrl: env["MAGENTO_GRAPHQL_URL"] || void 0,
214
+ reviewRatingCode: env["MAGENTO_REVIEW_RATING_CODE"] || void 0,
215
+ allCurrencies: []
216
+ };
217
+ }
218
+ function getMedusaConfiguration(env) {
219
+ return {
220
+ publishable_key: env["MEDUSA_PUBLISHABLE_KEY"] || "",
221
+ adminApiKey: env["MEDUSA_ADMIN_KEY"] || "",
222
+ apiUrl: env["MEDUSA_API_URL"] || "",
223
+ defaultCurrency: env["MEDUSA_DEFAULT_CURRENCY"] || "",
224
+ allCurrencies: []
225
+ };
226
+ }
227
+ function getMeilisearchConfiguration(env) {
228
+ return {
229
+ apiKey: env["MEILISEARCH_API_KEY"] || "",
230
+ apiUrl: env["MEILISEARCH_API_URL"] || "",
231
+ indexName: env["MEILISEARCH_INDEX"] || "",
232
+ useAIEmbedding: env["MEILISEARCH_USE_AI_EMBEDDING"] || void 0,
233
+ semanticRatio: env["MEILISEARCH_SEMANTIC_RATIO"] ? parseFloat(env["MEILISEARCH_SEMANTIC_RATIO"]) : 0.5,
234
+ orderIndexName: env["MEILISEARCH_ORDER_INDEX"] || "order",
235
+ useRecommendationsForBots: env["MEILISEARCH_USE_RECOMMENDATIONS_FOR_BOTS"] === "true"
236
+ };
237
+ }
238
+ export {
239
+ createReactionaryFeedClientFactoryFromEnv,
240
+ createReactionaryFeedClientFromEnv,
241
+ getEnabledReactionaryFeedProviderSystems,
242
+ getNoEnabledProviderSystemsMessage,
243
+ parseCommaSeparatedEnvList
244
+ };
@@ -0,0 +1,395 @@
1
+ import {
2
+ assertReactionaryFeedClient
3
+ } from "./feed-types.js";
4
+ class ReactionaryFeedGenerator {
5
+ constructor(client, options = {}) {
6
+ this.options = options;
7
+ assertReactionaryFeedClient(client);
8
+ this.client = client;
9
+ }
10
+ options;
11
+ client;
12
+ async *products(feed, requestContext) {
13
+ requestContext.languageContext = feed.languageContext;
14
+ const pageSize = feed.pageSize ?? feed.search.paginationOptions.pageSize ?? 50;
15
+ const maxPages = feed.maxPages ?? 100;
16
+ const productConcurrency = normalizeProductConcurrency(
17
+ this.options.productConcurrency
18
+ );
19
+ const startedAt = Date.now();
20
+ let processedProducts = 0;
21
+ let totalProducts = void 0;
22
+ let pageNumber = 1;
23
+ this.reportProgress({
24
+ phase: "searching",
25
+ processedProducts,
26
+ elapsedMs: Date.now() - startedAt
27
+ });
28
+ while (pageNumber <= maxPages) {
29
+ const result = await unwrapFeedResult(
30
+ this.client.productSearch.queryByTerm({
31
+ search: {
32
+ ...feed.search,
33
+ paginationOptions: {
34
+ ...feed.search.paginationOptions,
35
+ pageNumber,
36
+ pageSize
37
+ }
38
+ }
39
+ })
40
+ );
41
+ totalProducts = estimateTotalProducts(result, pageSize, maxPages);
42
+ const totalPages = Math.min(result.totalPages, maxPages);
43
+ this.reportProgress({
44
+ phase: "generating",
45
+ processedProducts,
46
+ totalProducts,
47
+ pageNumber,
48
+ totalPages,
49
+ elapsedMs: Date.now() - startedAt
50
+ });
51
+ for (const batch of chunks(result.items, productConcurrency)) {
52
+ const products = await Promise.all(
53
+ batch.map((item) => this.toFeedProduct(item, feed))
54
+ );
55
+ for (const product of products) {
56
+ if (product) {
57
+ processedProducts += 1;
58
+ this.reportProgress({
59
+ phase: "generating",
60
+ processedProducts,
61
+ totalProducts,
62
+ pageNumber,
63
+ totalPages,
64
+ elapsedMs: Date.now() - startedAt
65
+ });
66
+ yield product;
67
+ }
68
+ }
69
+ }
70
+ if (pageNumber >= result.totalPages || result.items.length === 0) {
71
+ this.reportProgress({
72
+ phase: "completed",
73
+ processedProducts,
74
+ totalProducts,
75
+ pageNumber,
76
+ totalPages,
77
+ elapsedMs: Date.now() - startedAt
78
+ });
79
+ return;
80
+ }
81
+ pageNumber += 1;
82
+ }
83
+ this.reportProgress({
84
+ phase: "completed",
85
+ processedProducts,
86
+ totalProducts,
87
+ pageNumber: pageNumber - 1,
88
+ totalPages: totalProducts === void 0 ? void 0 : maxPages,
89
+ elapsedMs: Date.now() - startedAt
90
+ });
91
+ }
92
+ async toFeedProduct(item, feed) {
93
+ const firstVariant = item.variants[0]?.variant;
94
+ if (!firstVariant) {
95
+ return void 0;
96
+ }
97
+ const productResult = await this.client.product.getBySKU({
98
+ variant: firstVariant
99
+ });
100
+ const product = productResult.success ? productResult.value : void 0;
101
+ const variants = await Promise.all(
102
+ item.variants.map(
103
+ (variant) => this.toFeedVariant(item, product, variant.variant.sku, feed)
104
+ )
105
+ );
106
+ const images = uniqueImages([
107
+ ...toFeedImages(product?.mainVariant.images ?? []),
108
+ ...item.variants.flatMap((variant) => toFeedImages([variant.image]))
109
+ ]);
110
+ const [categoryPath, reviewData] = product ? await Promise.all([
111
+ this.getCategoryPath(product),
112
+ this.getReviewData(product)
113
+ ]) : [void 0, void 0];
114
+ return {
115
+ id: product?.identifier.key ?? item.identifier.key,
116
+ title: product?.name ?? item.name,
117
+ description: product?.description || product?.longDescription,
118
+ url: toProductUrl(product?.slug ?? item.slug, feed),
119
+ brand: product?.brand,
120
+ manufacturer: product?.manufacturer,
121
+ ...categoryPath && categoryPath.length > 0 ? { categoryPath } : {},
122
+ ...reviewData?.ratingSummary ? { ratingSummary: reviewData.ratingSummary } : {},
123
+ ...reviewData?.reviews?.length ? { reviews: reviewData.reviews } : {},
124
+ images,
125
+ variants
126
+ };
127
+ }
128
+ async toFeedVariant(item, product, sku, feed) {
129
+ const variant = product?.variants.find(
130
+ (productVariant) => productVariant.identifier.sku === sku
131
+ ) ?? product?.mainVariant;
132
+ const [customerPriceResult, listPriceResult, inventory] = await Promise.all([
133
+ this.client.price.getCustomerPrice({ variant: { sku } }),
134
+ this.client.price.getListPrice({ variant: { sku } }),
135
+ this.getInventoryBySKU(sku, feed)
136
+ ]);
137
+ const customerPrice = customerPriceResult.success ? {
138
+ value: customerPriceResult.value.unitPrice.value,
139
+ currency: customerPriceResult.value.unitPrice.currency
140
+ } : void 0;
141
+ const listPrice = listPriceResult.success ? {
142
+ value: listPriceResult.value.unitPrice.value,
143
+ currency: listPriceResult.value.unitPrice.currency
144
+ } : void 0;
145
+ const availability = inventory ? toFeedAvailability(inventory) : void 0;
146
+ return {
147
+ id: sku,
148
+ title: variant?.name || item.name,
149
+ description: product?.description,
150
+ url: toProductUrl(product?.slug ?? item.slug, feed),
151
+ ...variant?.ean ? { ean: variant.ean } : {},
152
+ ...variant?.gtin ? { gtin: variant.gtin } : {},
153
+ ...variant?.upc ? { upc: variant.upc } : {},
154
+ ...variant?.barcode ? { barcode: variant.barcode } : {},
155
+ manufacturerPartNumber: sku,
156
+ images: uniqueImages(toFeedImages(variant?.images ?? [])),
157
+ price: customerPrice,
158
+ listPrice,
159
+ availability,
160
+ barcodes: toFeedBarcodes(variant),
161
+ options: variant?.options.map((option) => ({
162
+ name: option.name,
163
+ value: option.value.label || option.value.identifier.key
164
+ })) ?? []
165
+ };
166
+ }
167
+ async getInventoryBySKU(sku, feed) {
168
+ const fulfillmentCenterKeys = getFulfillmentCenterKeys(
169
+ feed,
170
+ this.options.defaultFulfillmentCenterKeys
171
+ );
172
+ const inventoryResults = await Promise.all(
173
+ fulfillmentCenterKeys.map(
174
+ (fulfillmentCenterKey) => this.client.inventory.getBySKU({
175
+ variant: { sku },
176
+ fulfilmentCenter: {
177
+ key: fulfillmentCenterKey
178
+ }
179
+ })
180
+ )
181
+ );
182
+ const inventories = inventoryResults.filter((result) => result.success).map((result) => result.value);
183
+ if (inventories.length === 0) {
184
+ return void 0;
185
+ }
186
+ if (inventories.length === 1) {
187
+ return inventories[0];
188
+ }
189
+ return combineInventory(sku, inventories);
190
+ }
191
+ async getCategoryPath(product) {
192
+ const category = this.client.category;
193
+ const getBreadcrumbPathToCategory = category?.getBreadcrumbPathToCategory;
194
+ const parentCategory = product.parentCategories[0];
195
+ if (!getBreadcrumbPathToCategory || !parentCategory) {
196
+ return void 0;
197
+ }
198
+ const result = await getBreadcrumbPathToCategory.call(category, {
199
+ id: parentCategory
200
+ });
201
+ if (!result.success) {
202
+ throw new Error(JSON.stringify(result.error));
203
+ }
204
+ return result.value.map(toFeedCategory);
205
+ }
206
+ async getReviewData(product) {
207
+ const productReviews = this.client.productReviews;
208
+ if (!productReviews) {
209
+ return void 0;
210
+ }
211
+ const [summaryResult, reviewsResult] = await Promise.all([
212
+ productReviews.getRatingSummary({
213
+ product: product.identifier
214
+ }),
215
+ productReviews.findReviews({
216
+ product: product.identifier,
217
+ paginationOptions: {
218
+ pageNumber: 1,
219
+ pageSize: 3
220
+ }
221
+ })
222
+ ]);
223
+ if (!summaryResult.success) {
224
+ throw new Error(JSON.stringify(summaryResult.error));
225
+ }
226
+ if (!reviewsResult.success) {
227
+ throw new Error(JSON.stringify(reviewsResult.error));
228
+ }
229
+ return {
230
+ ratingSummary: toFeedRatingSummary(summaryResult.value),
231
+ reviews: reviewsResult.value.items.map(toFeedReview)
232
+ };
233
+ }
234
+ reportProgress(progress) {
235
+ this.options.onProgress?.(progress);
236
+ }
237
+ }
238
+ async function unwrapFeedResult(resultPromise) {
239
+ const result = await resultPromise;
240
+ if (!result.success) {
241
+ throw new Error(JSON.stringify(result.error));
242
+ }
243
+ return result.value;
244
+ }
245
+ function toFeedAvailability(inventory) {
246
+ return {
247
+ available: inventory.status === "inStock" && inventory.quantity > 0,
248
+ status: toFeedAvailabilityStatus(inventory.status),
249
+ quantity: inventory.quantity
250
+ };
251
+ }
252
+ function toFeedCategory(category) {
253
+ return {
254
+ id: category.identifier.key,
255
+ name: category.name,
256
+ ...category.slug ? { slug: category.slug } : {}
257
+ };
258
+ }
259
+ function toFeedRatingSummary(summary) {
260
+ return {
261
+ averageRating: summary.averageRating,
262
+ ...summary.totalRatings !== void 0 ? { totalRatings: summary.totalRatings } : {},
263
+ ...summary.ratingDistribution ? { ratingDistribution: summary.ratingDistribution } : {}
264
+ };
265
+ }
266
+ function toFeedReview(review) {
267
+ return {
268
+ id: review.identifier.key,
269
+ authorName: review.authorName,
270
+ rating: review.rating,
271
+ title: review.title,
272
+ content: review.content,
273
+ createdAt: review.createdAt,
274
+ verified: review.verified
275
+ };
276
+ }
277
+ function estimateTotalProducts(result, pageSize, maxPages) {
278
+ return Math.min(result.totalCount, Math.min(result.totalPages, maxPages) * pageSize);
279
+ }
280
+ function normalizeProductConcurrency(value) {
281
+ if (!value || !Number.isFinite(value)) {
282
+ return 10;
283
+ }
284
+ return Math.max(1, Math.floor(value));
285
+ }
286
+ function chunks(items, size) {
287
+ const result = [];
288
+ for (let index = 0; index < items.length; index += size) {
289
+ result.push(items.slice(index, index + size));
290
+ }
291
+ return result;
292
+ }
293
+ function toFeedAvailabilityStatus(status) {
294
+ switch (status) {
295
+ case "inStock":
296
+ return "in_stock";
297
+ case "onBackOrder":
298
+ return "backorder";
299
+ case "preOrder":
300
+ return "preorder";
301
+ case "discontinued":
302
+ return "discontinued";
303
+ case "outOfStock":
304
+ default:
305
+ return "out_of_stock";
306
+ }
307
+ }
308
+ function getFulfillmentCenterKeys(feed, defaultFulfillmentCenterKeys) {
309
+ const keys = feed.fulfillmentCenterKeys?.length ? feed.fulfillmentCenterKeys : feed.fulfillmentCenterKey ? [feed.fulfillmentCenterKey] : defaultFulfillmentCenterKeys?.length ? defaultFulfillmentCenterKeys : [""];
310
+ return [...new Set(keys.map((key) => key.trim()))];
311
+ }
312
+ function combineInventory(sku, inventories) {
313
+ return {
314
+ identifier: {
315
+ variant: { sku },
316
+ fulfillmentCenter: {
317
+ key: inventories.map(
318
+ (inventory) => inventory.identifier.fulfillmentCenter.key
319
+ ).join(",")
320
+ }
321
+ },
322
+ quantity: inventories.reduce((total, inventory) => total + inventory.quantity, 0),
323
+ status: getCombinedInventoryStatus(inventories)
324
+ };
325
+ }
326
+ function getCombinedInventoryStatus(inventories) {
327
+ if (inventories.some(
328
+ (inventory) => inventory.status === "inStock" && inventory.quantity > 0
329
+ )) {
330
+ return "inStock";
331
+ }
332
+ if (inventories.some((inventory) => inventory.status === "onBackOrder")) {
333
+ return "onBackOrder";
334
+ }
335
+ if (inventories.some((inventory) => inventory.status === "preOrder")) {
336
+ return "preOrder";
337
+ }
338
+ if (inventories.every((inventory) => inventory.status === "discontinued")) {
339
+ return "discontinued";
340
+ }
341
+ return "outOfStock";
342
+ }
343
+ function toFeedImages(images) {
344
+ return images.filter((image) => image.sourceUrl.length > 0).map((image) => ({
345
+ url: image.sourceUrl,
346
+ ...image.altText ? { altText: image.altText } : {}
347
+ }));
348
+ }
349
+ function uniqueImages(images) {
350
+ const seen = /* @__PURE__ */ new Set();
351
+ return images.filter((image) => {
352
+ if (seen.has(image.url)) {
353
+ return false;
354
+ }
355
+ seen.add(image.url);
356
+ return true;
357
+ });
358
+ }
359
+ function toFeedBarcodes(variant) {
360
+ if (!variant) {
361
+ return [];
362
+ }
363
+ return [
364
+ ["ean", variant.ean],
365
+ ["gtin", variant.gtin],
366
+ ["upc", variant.upc],
367
+ ["barcode", variant.barcode]
368
+ ].filter(([, value]) => value.length > 0).map(([type, value]) => ({
369
+ type,
370
+ value
371
+ }));
372
+ }
373
+ function toProductUrl(slug, feed) {
374
+ if (!slug || !feed.productUrlBase) {
375
+ return void 0;
376
+ }
377
+ if (feed.productUrlBase.includes("{slug}") || feed.productUrlBase.includes("{lang}")) {
378
+ return feed.productUrlBase.replaceAll("{lang}", getLanguage(feed.languageContext)).replaceAll("{slug}", encodeURIComponent(slug));
379
+ }
380
+ return new URL(slug, feed.productUrlBase).href;
381
+ }
382
+ function getLanguage(feed) {
383
+ const [language] = feed.locale.split("-");
384
+ return language.toLowerCase();
385
+ }
386
+ function getTargetCountry(feed) {
387
+ const [, region] = feed.locale.split("-");
388
+ return region?.toUpperCase();
389
+ }
390
+ export {
391
+ ReactionaryFeedGenerator,
392
+ getLanguage,
393
+ getTargetCountry,
394
+ toProductUrl
395
+ };