@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,128 @@
1
+ import {
2
+ getTargetCountry
3
+ } from "../feed-generator.js";
4
+ import {
5
+ formatMoney,
6
+ primaryImage,
7
+ productDescription
8
+ } from "./shared.js";
9
+ const acpProductFeedTransformer = {
10
+ id: "acp-product-feed",
11
+ title: "ACP Product Feed",
12
+ description: "Agentic Commerce Protocol product feed output.",
13
+ output: {
14
+ contentType: "application/x-ndjson; charset=utf-8",
15
+ fileExtension: "jsonl"
16
+ },
17
+ defaultOptions: {
18
+ format: "jsonl"
19
+ },
20
+ async *transform(products, context) {
21
+ if (context.options.format === "json") {
22
+ const collected = [];
23
+ for await (const product of products) {
24
+ collected.push(...toACPRows(product, context.feed));
25
+ }
26
+ yield JSON.stringify({
27
+ ...getTargetCountry(context.feed.languageContext) ? { target_country: getTargetCountry(context.feed.languageContext) } : {},
28
+ products: collected
29
+ });
30
+ return;
31
+ }
32
+ for await (const product of products) {
33
+ for (const row of toACPRows(product, context.feed)) {
34
+ yield `${JSON.stringify(row)}
35
+ `;
36
+ }
37
+ }
38
+ }
39
+ };
40
+ function toACPRows(product, feed) {
41
+ return product.variants.map(
42
+ (variant) => toACPRow(product, variant, feed)
43
+ );
44
+ }
45
+ function toACPRow(product, variant, feed) {
46
+ const primaryImageUrl = primaryImage(product, variant);
47
+ const additionalImageUrls = getAdditionalImageUrls(product, variant, primaryImageUrl);
48
+ const variation = getVariationFields(product, variant);
49
+ const priceFields = getPriceFields(variant);
50
+ const gtin = variant.gtin || variant.ean || variant.upc || void 0;
51
+ return {
52
+ item_id: variant.id,
53
+ ...variation,
54
+ title: variant.title || product.title,
55
+ description: variant.description ?? productDescription(product),
56
+ ...variant.url ?? product.url ? { url: variant.url ?? product.url } : {},
57
+ ...product.brand ? { brand: product.brand } : {},
58
+ is_eligible_search: true,
59
+ ...feed.sellerName ? { seller_name: feed.sellerName } : {},
60
+ ...product.manufacturer ? { manufacturer: product.manufacturer } : {},
61
+ ...product.categoryPath?.length ? {
62
+ product_category: product.categoryPath.map((category) => category.name).join(" > ")
63
+ } : {},
64
+ ...product.ratingSummary ? {
65
+ ...product.ratingSummary.totalRatings !== void 0 ? { review_count: product.ratingSummary.totalRatings } : {},
66
+ ...product.ratingSummary.totalRatings ? { star_rating: product.ratingSummary.averageRating.toFixed(2) } : {}
67
+ } : {},
68
+ ...primaryImageUrl ? { image_url: primaryImageUrl } : {},
69
+ ...additionalImageUrls.length > 0 ? { additional_image_urls: additionalImageUrls } : {},
70
+ availability: toACPAvailability(variant.availability?.status),
71
+ ...priceFields,
72
+ ...gtin ? { gtin } : {},
73
+ ...variant.manufacturerPartNumber ? { mpn: variant.manufacturerPartNumber } : {}
74
+ };
75
+ }
76
+ function getVariationFields(product, variant) {
77
+ if (product.variants.length <= 1 || product.id === variant.id || variant.options.length === 0) {
78
+ return {};
79
+ }
80
+ const variantDict = Object.fromEntries(
81
+ variant.options.map((option) => [option.name, option.value])
82
+ );
83
+ return {
84
+ group_id: product.id,
85
+ listing_has_variations: true,
86
+ variant_dict: variantDict
87
+ };
88
+ }
89
+ function getPriceFields(variant) {
90
+ if (variant.price && variant.listPrice && variant.price.currency.toUpperCase() === variant.listPrice.currency.toUpperCase() && variant.price.value > 0 && variant.listPrice.value > variant.price.value) {
91
+ return {
92
+ price: formatMoney(variant.listPrice),
93
+ sale_price: formatMoney(variant.price)
94
+ };
95
+ }
96
+ return {
97
+ ...variant.price ?? variant.listPrice ? { price: formatMoney(variant.price ?? variant.listPrice) } : {}
98
+ };
99
+ }
100
+ function toACPAvailability(status) {
101
+ switch (status) {
102
+ case "in_stock":
103
+ case "out_of_stock":
104
+ case "backorder":
105
+ return status;
106
+ case "preorder":
107
+ return "pre_order";
108
+ default:
109
+ return "unknown";
110
+ }
111
+ }
112
+ function getAdditionalImageUrls(product, variant, primaryImageUrl) {
113
+ const urls = [
114
+ ...variant.images.map((image) => image.url),
115
+ ...product.images.map((image) => image.url)
116
+ ];
117
+ const unique = /* @__PURE__ */ new Set();
118
+ return urls.filter((url) => {
119
+ if (url === primaryImageUrl || unique.has(url)) {
120
+ return false;
121
+ }
122
+ unique.add(url);
123
+ return true;
124
+ });
125
+ }
126
+ export {
127
+ acpProductFeedTransformer
128
+ };
@@ -0,0 +1,60 @@
1
+ import { create } from "xmlbuilder2";
2
+ import {
3
+ googleMoney,
4
+ primaryImage,
5
+ productDescription
6
+ } from "./shared.js";
7
+ const googleMerchantFeedTransformer = {
8
+ id: "google-merchant-feed",
9
+ title: "Google Merchant Feed",
10
+ description: "Google Merchant Center RSS XML feed.",
11
+ output: {
12
+ contentType: "application/xml; charset=utf-8",
13
+ fileExtension: "xml"
14
+ },
15
+ async *transform(products) {
16
+ const document = create({ version: "1.0", encoding: "UTF-8" });
17
+ const channel = document.ele("rss", {
18
+ version: "2.0",
19
+ "xmlns:g": "http://base.google.com/ns/1.0"
20
+ }).ele("channel");
21
+ channel.ele("title").txt("Reactionary Product Feed").up();
22
+ for await (const product of products) {
23
+ for (const variant of product.variants) {
24
+ appendGoogleItem(channel, product, variant);
25
+ }
26
+ }
27
+ yield `${document.end({ prettyPrint: true })}
28
+ `;
29
+ }
30
+ };
31
+ function appendGoogleItem(channel, product, variant) {
32
+ const item = channel.ele("item");
33
+ const image = primaryImage(product, variant);
34
+ const link = variant.url ?? product.url;
35
+ const gtin = variant.barcodes.find((barcode) => barcode.type === "gtin")?.value;
36
+ item.ele("g:id").txt(variant.id).up();
37
+ item.ele("g:item_group_id").txt(product.id).up();
38
+ item.ele("title").txt(variant.title || product.title).up();
39
+ item.ele("description").txt(variant.description ?? productDescription(product)).up();
40
+ if (link) {
41
+ item.ele("link").txt(link).up();
42
+ }
43
+ if (image) {
44
+ item.ele("g:image_link").txt(image).up();
45
+ }
46
+ item.ele("g:availability").txt(variant.availability?.available ? "in stock" : "out of stock").up();
47
+ if (variant.price) {
48
+ item.ele("g:price").txt(googleMoney(variant.price) ?? "").up();
49
+ }
50
+ if (product.brand) {
51
+ item.ele("g:brand").txt(product.brand).up();
52
+ }
53
+ if (gtin) {
54
+ item.ele("g:gtin").txt(gtin).up();
55
+ }
56
+ item.ele("g:condition").txt("new").up();
57
+ }
58
+ export {
59
+ googleMerchantFeedTransformer
60
+ };
@@ -0,0 +1,60 @@
1
+ import { create } from "xmlbuilder2";
2
+ import {
3
+ formatMoney,
4
+ primaryImage,
5
+ productDescription
6
+ } from "./shared.js";
7
+ const pricerunnerFeedTransformer = {
8
+ id: "pricerunner-feed",
9
+ title: "PriceRunner XML Feed",
10
+ description: "PriceRunner Products XML feed.",
11
+ output: {
12
+ contentType: "application/xml; charset=utf-8",
13
+ fileExtension: "xml"
14
+ },
15
+ async *transform(products) {
16
+ const document = create({ version: "1.0", encoding: "UTF-8" });
17
+ const root = document.ele("Products");
18
+ for await (const product of products) {
19
+ for (const variant of product.variants) {
20
+ appendPriceRunnerProduct(root, product, variant);
21
+ }
22
+ }
23
+ yield `${document.end({ prettyPrint: true })}
24
+ `;
25
+ }
26
+ };
27
+ function appendPriceRunnerProduct(root, product, variant) {
28
+ const ean = variant.barcodes.find((barcode) => barcode.type === "ean")?.value ?? variant.barcodes.find((barcode) => barcode.type === "gtin")?.value;
29
+ const url = variant.url ?? product.url;
30
+ const image = primaryImage(product, variant);
31
+ const productElement = root.ele("Product");
32
+ productElement.ele("ProductId").txt(variant.id).up();
33
+ productElement.ele("ProductName").dat(variant.title || product.title).up();
34
+ if (variant.price) {
35
+ productElement.ele("Price").txt(formatMoney(variant.price) ?? "").up();
36
+ }
37
+ productElement.ele("StockStatus").txt(variant.availability?.available ? "in stock" : "out of stock").up();
38
+ if (product.brand) {
39
+ productElement.ele("Brand").dat(product.brand).up();
40
+ }
41
+ productElement.ele("Msku").txt(variant.id).up();
42
+ if (ean) {
43
+ productElement.ele("Ean").txt(ean).up();
44
+ }
45
+ if (url) {
46
+ productElement.ele("Url").txt(url).up();
47
+ }
48
+ if (image) {
49
+ productElement.ele("ImageUrl").txt(image).up();
50
+ }
51
+ productElement.ele("Description").dat(variant.description ?? productDescription(product)).up();
52
+ productElement.ele("Condition").txt("New").up();
53
+ productElement.ele("GroupId").txt(product.id).up();
54
+ for (const option of variant.options) {
55
+ productElement.ele(option.name).dat(option.value).up();
56
+ }
57
+ }
58
+ export {
59
+ pricerunnerFeedTransformer
60
+ };
@@ -0,0 +1,25 @@
1
+ function toMinorUnits(value) {
2
+ return Math.max(Math.round(value * 100), 0);
3
+ }
4
+ function formatMoney(money) {
5
+ if (!money) {
6
+ return void 0;
7
+ }
8
+ return `${money.value.toFixed(2)} ${money.currency.toUpperCase()}`;
9
+ }
10
+ function googleMoney(money) {
11
+ return formatMoney(money);
12
+ }
13
+ function primaryImage(product, variant) {
14
+ return variant?.images[0]?.url ?? product.images[0]?.url;
15
+ }
16
+ function productDescription(product) {
17
+ return product.description ?? product.title;
18
+ }
19
+ export {
20
+ formatMoney,
21
+ googleMoney,
22
+ primaryImage,
23
+ productDescription,
24
+ toMinorUnits
25
+ };
@@ -0,0 +1,26 @@
1
+ import { toSitemapXml } from "../sitemap-xml.js";
2
+ const sitemapFeedTransformer = {
3
+ id: "sitemap-feed",
4
+ title: "Sitemap XML Feed",
5
+ description: "XML sitemap of product URLs.",
6
+ output: {
7
+ contentType: "application/xml; charset=utf-8",
8
+ fileExtension: "xml"
9
+ },
10
+ async *transform(products) {
11
+ yield toSitemapXml(toProductSitemapEntries(products));
12
+ }
13
+ };
14
+ async function* toProductSitemapEntries(products) {
15
+ for await (const product of products) {
16
+ if (!product.url) {
17
+ continue;
18
+ }
19
+ yield {
20
+ url: product.url
21
+ };
22
+ }
23
+ }
24
+ export {
25
+ sitemapFeedTransformer
26
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@reactionary/feeds",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "./index.js",
6
+ "types": "./src/index.d.ts",
7
+ "bin": {
8
+ "reactionary-feeds": "./bin/reactionary-feeds"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/Solteq/reactionary.git"
13
+ },
14
+ "dependencies": {
15
+ "@reactionary/algolia": "0.0.1",
16
+ "@reactionary/commercetools": "0.0.1",
17
+ "@reactionary/core": "0.0.1",
18
+ "@reactionary/fake": "0.0.1",
19
+ "@reactionary/magento": "0.0.1",
20
+ "@reactionary/medusa": "0.0.1",
21
+ "@reactionary/meilisearch": "0.0.1",
22
+ "@reactionary/unomi": "0.0.1",
23
+ "commander": "^15.0.0",
24
+ "dotenv": "^17.2.2",
25
+ "sitemap": "^9.0.1",
26
+ "xmlbuilder2": "^4.0.3"
27
+ },
28
+ "devDependencies": {
29
+ "vitest": "^4.0.9"
30
+ },
31
+ "sideEffects": false
32
+ }
package/src/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/src/index.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export * from './lib/default-registry.js';
2
+ export * from './lib/env-client-builder.js';
3
+ export * from './lib/feed-generator.js';
4
+ export * from './lib/feed-server.js';
5
+ export * from './lib/feed-transformer.js';
6
+ export * from './lib/feed-types.js';
7
+ export * from './lib/sitemap-generator.js';
8
+ export * from './lib/sitemap-xml.js';
9
+ export * from './lib/load-cwd-env.js';
10
+ export * from './lib/transformers/acp-product-feed.transformer.js';
11
+ export * from './lib/transformers/google-merchant-feed.transformer.js';
12
+ export * from './lib/transformers/pricerunner-feed.transformer.js';
13
+ export * from './lib/transformers/sitemap-feed.transformer.js';
@@ -0,0 +1,2 @@
1
+ import { ReactionaryFeedTransformerRegistry } from './feed-transformer.js';
2
+ export declare function createDefaultFeedRegistry(): ReactionaryFeedTransformerRegistry;
@@ -0,0 +1,18 @@
1
+ import { type Cache, type RequestContext } from '@reactionary/core';
2
+ import type { ReactionaryFeedClient, ReactionaryFeedClientFactory } from './feed-types.js';
3
+ export type ReactionaryFeedProviderSystem = 'ALGOLIA' | 'COMMERCETOOLS' | 'FAKE' | 'MAGENTO' | 'MEDUSA' | 'MEILISEARCH' | 'UNOMI';
4
+ export interface CreateReactionaryFeedClientFromEnvOptions {
5
+ env?: NodeJS.ProcessEnv;
6
+ contextOverrides?: Partial<RequestContext>;
7
+ cache?: Cache;
8
+ loadEnv?: boolean;
9
+ }
10
+ export interface ReactionaryFeedClientFromEnv {
11
+ client: ReactionaryFeedClient;
12
+ enabledSystems: ReactionaryFeedProviderSystem[];
13
+ }
14
+ export declare function createReactionaryFeedClientFromEnv(options?: CreateReactionaryFeedClientFromEnvOptions): ReactionaryFeedClientFromEnv;
15
+ export declare function createReactionaryFeedClientFactoryFromEnv(options?: Omit<CreateReactionaryFeedClientFromEnvOptions, 'contextOverrides'>): ReactionaryFeedClientFactory;
16
+ export declare function getEnabledReactionaryFeedProviderSystems(env?: NodeJS.ProcessEnv): ReactionaryFeedProviderSystem[];
17
+ export declare function getNoEnabledProviderSystemsMessage(): string;
18
+ export declare function parseCommaSeparatedEnvList(value: string | undefined): string[];
@@ -0,0 +1,17 @@
1
+ import type { RequestContext } from '@reactionary/core';
2
+ import { type ReactionaryFeedClient, type ReactionaryFeedDefinition, type ReactionaryFeedGeneratorOptions, type ReactionaryFeedProduct } from './feed-types.js';
3
+ export declare class ReactionaryFeedGenerator {
4
+ private readonly options;
5
+ private readonly client;
6
+ constructor(client: ReactionaryFeedClient, options?: ReactionaryFeedGeneratorOptions);
7
+ products(feed: ReactionaryFeedDefinition, requestContext: RequestContext): AsyncGenerator<ReactionaryFeedProduct>;
8
+ private toFeedProduct;
9
+ private toFeedVariant;
10
+ private getInventoryBySKU;
11
+ private getCategoryPath;
12
+ private getReviewData;
13
+ private reportProgress;
14
+ }
15
+ export declare function toProductUrl(slug: string | undefined, feed: ReactionaryFeedDefinition): string | undefined;
16
+ export declare function getLanguage(feed: ReactionaryFeedDefinition['languageContext']): string;
17
+ export declare function getTargetCountry(feed: ReactionaryFeedDefinition['languageContext']): string | undefined;
@@ -0,0 +1,28 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { ReactionaryFeedTransformerRegistry } from './feed-transformer.js';
3
+ import type { ReactionaryFeedClient, ReactionaryFeedClientFactory, ReactionaryFeedDefinition, ReactionaryFeedInventoryOptions, ReactionaryFeedProcessingOptions, ReactionarySitemapOptions } from './feed-types.js';
4
+ export interface ReactionaryFeedServerOptions extends ReactionaryFeedInventoryOptions, ReactionaryFeedProcessingOptions {
5
+ basePath?: string;
6
+ feeds: Record<string, ReactionaryFeedDefinition>;
7
+ sitemaps?: ReactionarySitemapOptions;
8
+ registry?: ReactionaryFeedTransformerRegistry;
9
+ }
10
+ export interface ReactionaryFeedHttpHandler {
11
+ fetch(request: Request): Promise<Response>;
12
+ close(): Promise<void>;
13
+ }
14
+ export type ReactionaryFeedNodeRequestHandler = (request: IncomingMessage, response: ServerResponse) => Promise<void>;
15
+ export declare class ReactionaryFeedServer<TClient extends ReactionaryFeedClient = ReactionaryFeedClient> {
16
+ private readonly clientFactory;
17
+ private readonly options;
18
+ private readonly registry;
19
+ constructor(clientFactory: ReactionaryFeedClientFactory<TClient>, options: ReactionaryFeedServerOptions);
20
+ fetch(request: Request): Promise<Response>;
21
+ getHandler(): ReactionaryFeedHttpHandler;
22
+ toNodeHandler(): ReactionaryFeedNodeRequestHandler;
23
+ close(): Promise<void>;
24
+ private handleRequest;
25
+ private generateFeedResponse;
26
+ private generateSitemapIndexResponse;
27
+ private generateSitemapResponse;
28
+ }
@@ -0,0 +1,30 @@
1
+ import type { ReactionaryFeedDefinition, ReactionaryFeedProduct } from './feed-types.js';
2
+ export interface ReactionaryFeedTransformContext<TOptions = unknown> {
3
+ feedId: string;
4
+ feed: ReactionaryFeedDefinition;
5
+ options: TOptions;
6
+ }
7
+ export interface ReactionaryFeedTransformer<TOptions = unknown> {
8
+ id: string;
9
+ title: string;
10
+ description?: string;
11
+ output: {
12
+ contentType: string;
13
+ fileExtension: string;
14
+ };
15
+ defaultOptions?: TOptions;
16
+ transform(products: AsyncIterable<ReactionaryFeedProduct>, context: ReactionaryFeedTransformContext<TOptions>): AsyncIterable<string | Uint8Array>;
17
+ }
18
+ export interface ReactionaryFeedTransformerSummary {
19
+ id: string;
20
+ title: string;
21
+ description?: string;
22
+ contentType: string;
23
+ fileExtension: string;
24
+ }
25
+ export declare class ReactionaryFeedTransformerRegistry {
26
+ private readonly transformers;
27
+ register(transformer: ReactionaryFeedTransformer): void;
28
+ get(id: string): ReactionaryFeedTransformer | undefined;
29
+ list(): ReactionaryFeedTransformerSummary[];
30
+ }
@@ -0,0 +1,185 @@
1
+ import type { Category, CategoryPaginatedResult, Cache, Inventory, LanguageContext, Price, Product, ProductRatingSummary, ProductReviewPaginatedResult, ProductSearchResult, RequestContext, Result, SearchIdentifier, Store } from '@reactionary/core';
2
+ export interface ReactionaryFeedClient {
3
+ cache?: Cache;
4
+ productSearch?: {
5
+ queryByTerm(payload: unknown): Promise<Result<ProductSearchResult>>;
6
+ };
7
+ product?: {
8
+ getBySKU(payload: unknown): Promise<Result<Product>>;
9
+ };
10
+ productReviews?: {
11
+ getRatingSummary(payload: unknown): Promise<Result<ProductRatingSummary>>;
12
+ findReviews(payload: unknown): Promise<Result<ProductReviewPaginatedResult>>;
13
+ };
14
+ price?: {
15
+ getCustomerPrice(payload: unknown): Promise<Result<Price>>;
16
+ getListPrice(payload: unknown): Promise<Result<Price>>;
17
+ };
18
+ inventory?: {
19
+ getBySKU(payload: unknown): Promise<Result<Inventory>>;
20
+ };
21
+ category?: {
22
+ getBreadcrumbPathToCategory?(payload: unknown): Promise<Result<Category[]>>;
23
+ findTopCategories(payload: unknown): Promise<Result<CategoryPaginatedResult>>;
24
+ findChildCategories(payload: unknown): Promise<Result<CategoryPaginatedResult>>;
25
+ };
26
+ store?: {
27
+ queryByProximity(payload: unknown): Promise<Result<Store[]>>;
28
+ };
29
+ }
30
+ export type ValidatedReactionaryFeedClient = ReactionaryFeedClient & {
31
+ productSearch: NonNullable<ReactionaryFeedClient['productSearch']>;
32
+ product: NonNullable<ReactionaryFeedClient['product']>;
33
+ price: NonNullable<ReactionaryFeedClient['price']>;
34
+ inventory: NonNullable<ReactionaryFeedClient['inventory']>;
35
+ };
36
+ export type ReactionaryFeedClientFactory<TClient extends ReactionaryFeedClient = ReactionaryFeedClient> = (requestContext: RequestContext) => TClient;
37
+ export interface ReactionaryFeedInventoryOptions {
38
+ defaultFulfillmentCenterKeys?: string[];
39
+ }
40
+ export interface ReactionaryFeedProcessingOptions {
41
+ productConcurrency?: number;
42
+ }
43
+ export interface ReactionaryFeedProgress {
44
+ phase: 'searching' | 'generating' | 'completed';
45
+ processedProducts: number;
46
+ totalProducts?: number;
47
+ pageNumber?: number;
48
+ totalPages?: number;
49
+ elapsedMs: number;
50
+ }
51
+ export interface ReactionaryFeedGeneratorOptions extends ReactionaryFeedInventoryOptions, ReactionaryFeedProcessingOptions {
52
+ onProgress?: (progress: ReactionaryFeedProgress) => void;
53
+ }
54
+ export interface ReactionaryFeedDefinition {
55
+ languageContext: LanguageContext;
56
+ search: SearchIdentifier;
57
+ pageSize?: number;
58
+ maxPages?: number;
59
+ fulfillmentCenterKeys?: string[];
60
+ fulfillmentCenterKey?: string;
61
+ productUrlBase?: string;
62
+ sellerName?: string;
63
+ metadata?: Record<string, unknown>;
64
+ }
65
+ export type ReactionarySitemapChangeFrequency = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
66
+ export interface ReactionarySitemapEntry {
67
+ url: string;
68
+ lastmod?: Date | string;
69
+ changefreq?: ReactionarySitemapChangeFrequency;
70
+ priority?: number;
71
+ }
72
+ export interface ReactionarySitemapBaseDefinition {
73
+ languageContext: LanguageContext;
74
+ changefreq?: ReactionarySitemapChangeFrequency;
75
+ priority?: number;
76
+ }
77
+ export interface ReactionaryProductSitemapDefinition extends Partial<ReactionarySitemapBaseDefinition> {
78
+ type: 'products';
79
+ feed: string;
80
+ }
81
+ export interface ReactionaryCategorySitemapDefinition extends ReactionarySitemapBaseDefinition {
82
+ type: 'categories';
83
+ urlTemplate: string;
84
+ pageSize?: number;
85
+ maxPagesPerLevel?: number;
86
+ maxDepth?: number;
87
+ includeChildren?: boolean;
88
+ }
89
+ export interface ReactionaryStoreSitemapDefinition extends ReactionarySitemapBaseDefinition {
90
+ type: 'stores';
91
+ urlTemplate: string;
92
+ proximity: {
93
+ longitude: number;
94
+ latitude: number;
95
+ distance: number;
96
+ limit: number;
97
+ };
98
+ }
99
+ export type ReactionarySitemapDefinition = ReactionaryProductSitemapDefinition | ReactionaryCategorySitemapDefinition | ReactionaryStoreSitemapDefinition;
100
+ export interface ReactionarySitemapOptions {
101
+ baseUrl: string;
102
+ sources: Record<string, ReactionarySitemapDefinition>;
103
+ include?: string[];
104
+ }
105
+ export interface ReactionaryFeedImage {
106
+ url: string;
107
+ altText?: string;
108
+ }
109
+ export interface ReactionaryFeedMoney {
110
+ value: number;
111
+ currency: string;
112
+ }
113
+ export interface ReactionaryFeedBarcode {
114
+ type: 'ean' | 'gtin' | 'upc' | 'barcode';
115
+ value: string;
116
+ }
117
+ export interface ReactionaryFeedVariantOption {
118
+ name: string;
119
+ value: string;
120
+ }
121
+ export interface ReactionaryFeedAvailability {
122
+ available: boolean;
123
+ status: 'in_stock' | 'out_of_stock' | 'backorder' | 'preorder' | 'discontinued';
124
+ quantity?: number;
125
+ }
126
+ export interface ReactionaryFeedCategory {
127
+ id: string;
128
+ name: string;
129
+ slug?: string;
130
+ }
131
+ export interface ReactionaryFeedRatingSummary {
132
+ averageRating: number;
133
+ totalRatings?: number;
134
+ ratingDistribution?: Record<string, number>;
135
+ }
136
+ export interface ReactionaryFeedReview {
137
+ id: string;
138
+ authorName: string;
139
+ rating: number;
140
+ title: string;
141
+ content: string;
142
+ createdAt: string;
143
+ verified: boolean;
144
+ }
145
+ export interface ReactionaryFeedVariant {
146
+ id: string;
147
+ title: string;
148
+ description?: string;
149
+ url?: string;
150
+ ean?: string;
151
+ gtin?: string;
152
+ upc?: string;
153
+ barcode?: string;
154
+ manufacturerPartNumber?: string;
155
+ images: ReactionaryFeedImage[];
156
+ price?: ReactionaryFeedMoney;
157
+ listPrice?: ReactionaryFeedMoney;
158
+ availability?: ReactionaryFeedAvailability;
159
+ barcodes: ReactionaryFeedBarcode[];
160
+ options: ReactionaryFeedVariantOption[];
161
+ metadata?: Record<string, unknown>;
162
+ }
163
+ export interface ReactionaryFeedProduct {
164
+ id: string;
165
+ title: string;
166
+ description?: string;
167
+ url?: string;
168
+ brand?: string;
169
+ manufacturer?: string;
170
+ categoryPath?: ReactionaryFeedCategory[];
171
+ ratingSummary?: ReactionaryFeedRatingSummary;
172
+ reviews?: ReactionaryFeedReview[];
173
+ images: ReactionaryFeedImage[];
174
+ variants: ReactionaryFeedVariant[];
175
+ metadata?: Record<string, unknown>;
176
+ }
177
+ export interface ReactionaryFeedGenerateInput {
178
+ feedId: string;
179
+ feed: ReactionaryFeedDefinition;
180
+ requestContext: RequestContext;
181
+ }
182
+ export declare function assertReactionaryFeedClient(client: ReactionaryFeedClient): asserts client is ValidatedReactionaryFeedClient;
183
+ export declare function getMissingFeedClientOperations(client: ReactionaryFeedClient): string[];
184
+ export declare function getMissingSitemapClientOperations(client: ReactionaryFeedClient, source: ReactionarySitemapDefinition): string[];
185
+ export declare function assertReactionarySitemapClient(client: ReactionaryFeedClient, source: ReactionarySitemapDefinition): void;
@@ -0,0 +1,4 @@
1
+ export interface LoadCwdEnvOptions {
2
+ env?: NodeJS.ProcessEnv;
3
+ }
4
+ export declare function loadCwdEnv(options?: LoadCwdEnvOptions): void;
@@ -0,0 +1,20 @@
1
+ import type { RequestContext } from '@reactionary/core';
2
+ import type { ReactionaryFeedClient, ReactionaryFeedDefinition, ReactionaryFeedGeneratorOptions, ReactionarySitemapDefinition, ReactionarySitemapEntry } from './feed-types.js';
3
+ export declare class ReactionarySitemapGenerator {
4
+ private readonly client;
5
+ private readonly feeds;
6
+ private readonly options;
7
+ constructor(client: ReactionaryFeedClient, feeds: Record<string, ReactionaryFeedDefinition>, options?: ReactionaryFeedGeneratorOptions);
8
+ entries(source: ReactionarySitemapDefinition, requestContext: RequestContext): AsyncGenerator<ReactionarySitemapEntry>;
9
+ private productEntries;
10
+ private categoryEntries;
11
+ private categoryWithChildrenEntries;
12
+ private iterCategoryPageResults;
13
+ private storeEntries;
14
+ }
15
+ export declare function toTemplatedUrl(urlTemplate: string, languageContext: {
16
+ locale: string;
17
+ }, values: {
18
+ id: string;
19
+ slug?: string;
20
+ }): string | undefined;
@@ -0,0 +1,3 @@
1
+ import type { ReactionarySitemapEntry } from './feed-types.js';
2
+ export declare function toSitemapXml(entries: AsyncIterable<ReactionarySitemapEntry>): Promise<string>;
3
+ export declare function toSitemapIndexXml(urls: Iterable<string>): Promise<string>;
@@ -0,0 +1,4 @@
1
+ import type { ReactionaryFeedTransformer } from '../feed-transformer.js';
2
+ export declare const acpProductFeedTransformer: ReactionaryFeedTransformer<{
3
+ format?: 'jsonl' | 'json';
4
+ }>;