medusa-product-feed 0.1.0

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 ADDED
@@ -0,0 +1,122 @@
1
+ # medusa-product-feed
2
+
3
+ A self-contained Medusa v2 plugin that generates a Google Merchant Center–compatible XML product feed at `GET /store/feed`. Ships its own API route and cache-invalidation subscriber — the consuming project needs zero extra files.
4
+
5
+ ## What it does
6
+
7
+ - Generates a Google Shopping RSS XML feed from your Medusa product catalog
8
+ - Caches the feed in Redis and auto-invalidates on `product.created`, `product.updated`, and `product.deleted` events
9
+ - Supports three variant rendering modes: `parent`, `children`, and `config` (runtime query param)
10
+ - Reads all configuration from environment variables
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install medusa-product-feed
16
+ # or with pnpm
17
+ pnpm add medusa-product-feed
18
+ ```
19
+
20
+ For local development with a symlinked package:
21
+
22
+ ```bash
23
+ cd /path/to/shared-medusa-utils/medusa-product-feed && npm run build
24
+ cd /path/to/your-medusa-backend && pnpm link /path/to/shared-medusa-utils/medusa-product-feed
25
+ ```
26
+
27
+ ## Configuration
28
+
29
+ Set environment variables in your `.env`:
30
+
31
+ | Variable | Default | Description |
32
+ |-----------------------|----------------------------|----------------------------------------------------------------|
33
+ | `FEED_STOREFRONT_URL` | `$STOREFRONT_URL` or `http://localhost:8000` | Base URL for product links in the feed |
34
+ | `FEED_CURRENCY` | `ILS` | Currency code for prices |
35
+ | `FEED_REGION_ID` | _(empty)_ | Medusa region ID to use for pricing |
36
+ | `FEED_VARIANT_MODE` | `children` | Variant rendering mode: `parent`, `children`, or `config` |
37
+ | `FEED_CACHE_TTL` | `3600` | Cache TTL in seconds |
38
+ | `FEED_DISABLE_CACHE` | `false` | Set to `true` to disable Redis caching (useful in development) |
39
+ | `FEED_STORE_NAME` | _(none)_ | Store name included in the feed `<title>` |
40
+
41
+ ## Usage
42
+
43
+ Register the plugin in your `medusa-config.js` (or `medusa-config.ts`):
44
+
45
+ ```js
46
+ module.exports = defineConfig({
47
+ plugins: [
48
+ {
49
+ resolve: "medusa-product-feed",
50
+ options: {},
51
+ },
52
+ ],
53
+ // ...
54
+ })
55
+ ```
56
+
57
+ That's it. No extra files needed in your project. The plugin automatically registers:
58
+
59
+ - `GET /store/feed` — the XML product feed endpoint
60
+ - A subscriber that invalidates the feed cache on product changes
61
+
62
+ ## Feed URL
63
+
64
+ ```
65
+ GET /store/feed
66
+ ```
67
+
68
+ Returns `application/xml` with a Google Merchant Center–compatible RSS feed.
69
+
70
+ Response headers:
71
+ - `X-Feed-Cache: HIT` — served from Redis cache
72
+ - `X-Feed-Cache: MISS` — freshly generated and cached
73
+ - `X-Feed-Cache: DISABLED` — caching is disabled
74
+
75
+ ## Variant modes
76
+
77
+ | Mode | Behavior |
78
+ |------------|--------------------------------------------------------------------------|
79
+ | `parent` | One feed item per product (uses the first variant for price/SKU) |
80
+ | `children` | One feed item per variant (each variant is a separate `<item>`) |
81
+ | `config` | Resolved at request time via `?variants=parent` or `?variants=children` |
82
+
83
+ When `FEED_VARIANT_MODE=config`, callers control the mode per request:
84
+
85
+ ```
86
+ GET /store/feed?variants=children
87
+ GET /store/feed?variants=parent
88
+ ```
89
+
90
+ ## Cache behavior
91
+
92
+ The feed is cached in Redis using the Medusa cache module. Cache keys:
93
+
94
+ - `product-feed` — used for `parent` and `children` modes
95
+ - `product-feed:parent` / `product-feed:children` — used when mode is `config`
96
+
97
+ All keys are invalidated automatically when any product is created, updated, or deleted.
98
+
99
+ To disable caching (e.g. in development):
100
+
101
+ ```env
102
+ FEED_DISABLE_CACHE=true
103
+ ```
104
+
105
+ ## Advanced: custom config per request
106
+
107
+ If you need custom configuration beyond what env vars provide (e.g. multi-tenant logic), you can use the `createFeedRouteHandler` factory directly in your own route file:
108
+
109
+ ```ts
110
+ // src/api/store/feed/route.ts
111
+ import { createFeedRouteHandler } from "medusa-product-feed"
112
+
113
+ export const GET = createFeedRouteHandler({
114
+ storefrontUrl: "https://mystore.com",
115
+ currency: "USD",
116
+ regionId: "reg_01ABC",
117
+ variantMode: "children",
118
+ cacheTtlSeconds: 1800,
119
+ disableCache: false,
120
+ storeName: "My Store",
121
+ })
122
+ ```
@@ -0,0 +1 @@
1
+ export declare const GET: (req: import("@medusajs/framework").MedusaRequest, res: import("@medusajs/framework").MedusaResponse) => Promise<void>;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GET = void 0;
4
+ const route_1 = require("../../../route");
5
+ const types_1 = require("../../../types");
6
+ exports.GET = (0, route_1.createFeedRouteHandler)((0, types_1.feedConfigFromEnv)());
@@ -0,0 +1,2 @@
1
+ import type { FeedConfig, FeedProduct } from "./types";
2
+ export declare function buildGoogleMerchantFeed(products: FeedProduct[], config: FeedConfig): string;
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildGoogleMerchantFeed = buildGoogleMerchantFeed;
4
+ function escapeXml(str) {
5
+ return str
6
+ .replace(/&/g, "&amp;")
7
+ .replace(/</g, "&lt;")
8
+ .replace(/>/g, "&gt;")
9
+ .replace(/"/g, "&quot;")
10
+ .replace(/'/g, "&apos;");
11
+ }
12
+ function stripHtml(html) {
13
+ return html.replace(/<[^>]*>/g, "").trim();
14
+ }
15
+ function formatPrice(amount, currency) {
16
+ return `${(amount / 100).toFixed(2)} ${currency.toUpperCase()}`;
17
+ }
18
+ function getPrice(variant, currency) {
19
+ const price = variant.prices.find((p) => p.currency_code.toLowerCase() === currency.toLowerCase());
20
+ return price ? formatPrice(price.amount, currency) : null;
21
+ }
22
+ function getAvailability(variant) {
23
+ if (!variant.manage_inventory)
24
+ return "in stock";
25
+ return variant.inventory_quantity > 0 ? "in stock" : "out of stock";
26
+ }
27
+ function getBrand(product, storeName) {
28
+ if (product.metadata?.brand)
29
+ return String(product.metadata.brand);
30
+ if (product.collection?.title)
31
+ return product.collection.title;
32
+ return storeName;
33
+ }
34
+ function buildItem(id, itemGroupId, product, variant, config) {
35
+ const title = itemGroupId
36
+ ? `${product.title}${variant.title ? ` - ${variant.title}` : ""}`
37
+ : product.title;
38
+ const description = product.description ? stripHtml(product.description) : "";
39
+ const brand = getBrand(product, config.storeName ?? "");
40
+ const price = getPrice(variant, config.currency);
41
+ const availability = getAvailability(variant);
42
+ const link = `${config.storefrontUrl}/products/${product.handle}`;
43
+ const imageLink = product.images[0]?.url ?? "";
44
+ const additionalImages = product.images
45
+ .slice(1, 10)
46
+ .map((img) => ` <g:additional_image_link>${escapeXml(img.url)}</g:additional_image_link>`)
47
+ .join("\n");
48
+ const category = product.metadata?.google_category
49
+ ? ` <g:google_product_category>${escapeXml(String(product.metadata.google_category))}</g:google_product_category>`
50
+ : "";
51
+ return [
52
+ " <item>",
53
+ ` <g:id>${escapeXml(id)}</g:id>`,
54
+ itemGroupId ? ` <g:item_group_id>${escapeXml(itemGroupId)}</g:item_group_id>` : null,
55
+ ` <title>${escapeXml(title)}</title>`,
56
+ ` <description>${escapeXml(description)}</description>`,
57
+ ` <link>${escapeXml(link)}</link>`,
58
+ imageLink ? ` <g:image_link>${escapeXml(imageLink)}</g:image_link>` : null,
59
+ additionalImages || null,
60
+ price ? ` <g:price>${escapeXml(price)}</g:price>` : null,
61
+ ` <g:availability>${availability}</g:availability>`,
62
+ ` <g:condition>${escapeXml(String(product.metadata?.condition ?? "new"))}</g:condition>`,
63
+ ` <g:brand>${escapeXml(brand)}</g:brand>`,
64
+ category || null,
65
+ " </item>",
66
+ ]
67
+ .filter(Boolean)
68
+ .join("\n");
69
+ }
70
+ function buildGoogleMerchantFeed(products, config) {
71
+ if (config.variantMode === "config") {
72
+ throw new Error('buildGoogleMerchantFeed: variantMode "config" must be resolved to "parent" or "children" before calling this function');
73
+ }
74
+ const items = [];
75
+ for (const product of products) {
76
+ if (config.variantMode === "parent") {
77
+ const firstVariant = product.variants[0];
78
+ if (!firstVariant)
79
+ continue;
80
+ items.push(buildItem(product.id, null, product, firstVariant, config));
81
+ }
82
+ else {
83
+ for (const variant of product.variants) {
84
+ items.push(buildItem(variant.id, product.id, product, variant, config));
85
+ }
86
+ }
87
+ }
88
+ const storeTitle = escapeXml(config.storeName ?? "Product Feed");
89
+ const storeUrl = escapeXml(config.storefrontUrl);
90
+ return [
91
+ '<?xml version="1.0" encoding="UTF-8"?>',
92
+ '<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">',
93
+ "<channel>",
94
+ ` <title>${storeTitle}</title>`,
95
+ ` <link>${storeUrl}</link>`,
96
+ ` <description>${storeTitle} product feed for Google Merchant Center</description>`,
97
+ ...items,
98
+ "</channel>",
99
+ "</rss>",
100
+ ].join("\n");
101
+ }
@@ -0,0 +1,9 @@
1
+ interface CacheService {
2
+ get<T>(key: string): Promise<T | null>;
3
+ set(key: string, value: unknown, ttl?: number): Promise<void>;
4
+ invalidate(key: string): Promise<void>;
5
+ }
6
+ export declare function getFeed(cache: CacheService, key?: string): Promise<string | null>;
7
+ export declare function setFeed(cache: CacheService, xml: string, ttlSeconds: number, key?: string): Promise<void>;
8
+ export declare function invalidateFeed(cache: CacheService, key?: string): Promise<void>;
9
+ export {};
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getFeed = getFeed;
4
+ exports.setFeed = setFeed;
5
+ exports.invalidateFeed = invalidateFeed;
6
+ const DEFAULT_CACHE_KEY = "product-feed";
7
+ async function getFeed(cache, key = DEFAULT_CACHE_KEY) {
8
+ return cache.get(key);
9
+ }
10
+ async function setFeed(cache, xml, ttlSeconds, key = DEFAULT_CACHE_KEY) {
11
+ await cache.set(key, xml, ttlSeconds);
12
+ }
13
+ async function invalidateFeed(cache, key = DEFAULT_CACHE_KEY) {
14
+ await cache.invalidate(key);
15
+ }
@@ -0,0 +1,8 @@
1
+ import type { FeedProduct } from "./types";
2
+ interface RemoteQuery {
3
+ graph(options: object): Promise<{
4
+ data: any[];
5
+ }>;
6
+ }
7
+ export declare function fetchFeedProducts(query: RemoteQuery, currency: string, regionId: string): Promise<FeedProduct[]>;
8
+ export {};
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchFeedProducts = fetchFeedProducts;
4
+ const utils_1 = require("@medusajs/framework/utils");
5
+ async function fetchFeedProducts(query, currency, regionId) {
6
+ if (!regionId) {
7
+ console.warn("[medusa-product-feed] FEED_REGION_ID is not set — prices may not resolve correctly");
8
+ }
9
+ const pricingContext = { currency_code: currency.toLowerCase() };
10
+ if (regionId)
11
+ pricingContext.region_id = regionId;
12
+ const { data: products } = await query.graph({
13
+ entity: "product",
14
+ fields: [
15
+ "id",
16
+ "title",
17
+ "handle",
18
+ "description",
19
+ "status",
20
+ "metadata",
21
+ "images.url",
22
+ "collection.title",
23
+ "variants.id",
24
+ "variants.title",
25
+ "variants.sku",
26
+ "variants.manage_inventory",
27
+ "variants.inventory_quantity",
28
+ "variants.calculated_price.calculated_amount",
29
+ "variants.calculated_price.currency_code",
30
+ ],
31
+ filters: { status: "published" },
32
+ context: {
33
+ variants: {
34
+ calculated_price: (0, utils_1.QueryContext)(pricingContext),
35
+ },
36
+ },
37
+ });
38
+ return products.map((p) => ({
39
+ id: p.id,
40
+ title: p.title ?? "",
41
+ handle: p.handle ?? "",
42
+ description: p.description ?? null,
43
+ images: (p.images ?? []).map((img) => ({ url: img.url })),
44
+ collection: p.collection ? { title: p.collection.title } : null,
45
+ variants: (p.variants ?? []).map((v) => ({
46
+ id: v.id,
47
+ title: v.title ?? null,
48
+ sku: v.sku ?? null,
49
+ // calculated_amount is in display units (e.g. 99.00), multiply by 100 to match FeedVariant.prices minor-unit convention
50
+ prices: v.calculated_price?.calculated_amount != null
51
+ ? [{ currency_code: currency.toLowerCase(), amount: Math.round(v.calculated_price.calculated_amount * 100) }]
52
+ : [],
53
+ manage_inventory: v.manage_inventory ?? false,
54
+ inventory_quantity: v.inventory_quantity ?? 0,
55
+ })),
56
+ metadata: p.metadata ?? null,
57
+ }));
58
+ }
@@ -0,0 +1,7 @@
1
+ export type { FeedConfig, FeedProduct, FeedVariant, VariantMode } from "./types";
2
+ export { feedConfigFromEnv } from "./types";
3
+ export { buildGoogleMerchantFeed } from "./feed-builder";
4
+ export { getFeed, setFeed, invalidateFeed } from "./feed-cache";
5
+ export { fetchFeedProducts } from "./feed-query";
6
+ export { createFeedRouteHandler } from "./route";
7
+ export { default as productFeedInvalidationSubscriber, config as subscriberConfig } from "./subscribers/product-feed-invalidation";
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.subscriberConfig = exports.productFeedInvalidationSubscriber = exports.createFeedRouteHandler = exports.fetchFeedProducts = exports.invalidateFeed = exports.setFeed = exports.getFeed = exports.buildGoogleMerchantFeed = exports.feedConfigFromEnv = void 0;
7
+ var types_1 = require("./types");
8
+ Object.defineProperty(exports, "feedConfigFromEnv", { enumerable: true, get: function () { return types_1.feedConfigFromEnv; } });
9
+ var feed_builder_1 = require("./feed-builder");
10
+ Object.defineProperty(exports, "buildGoogleMerchantFeed", { enumerable: true, get: function () { return feed_builder_1.buildGoogleMerchantFeed; } });
11
+ var feed_cache_1 = require("./feed-cache");
12
+ Object.defineProperty(exports, "getFeed", { enumerable: true, get: function () { return feed_cache_1.getFeed; } });
13
+ Object.defineProperty(exports, "setFeed", { enumerable: true, get: function () { return feed_cache_1.setFeed; } });
14
+ Object.defineProperty(exports, "invalidateFeed", { enumerable: true, get: function () { return feed_cache_1.invalidateFeed; } });
15
+ var feed_query_1 = require("./feed-query");
16
+ Object.defineProperty(exports, "fetchFeedProducts", { enumerable: true, get: function () { return feed_query_1.fetchFeedProducts; } });
17
+ var route_1 = require("./route");
18
+ Object.defineProperty(exports, "createFeedRouteHandler", { enumerable: true, get: function () { return route_1.createFeedRouteHandler; } });
19
+ var product_feed_invalidation_1 = require("./subscribers/product-feed-invalidation");
20
+ Object.defineProperty(exports, "productFeedInvalidationSubscriber", { enumerable: true, get: function () { return __importDefault(product_feed_invalidation_1).default; } });
21
+ Object.defineProperty(exports, "subscriberConfig", { enumerable: true, get: function () { return product_feed_invalidation_1.config; } });
@@ -0,0 +1,3 @@
1
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2
+ import type { FeedConfig } from "./types";
3
+ export declare function createFeedRouteHandler(config: FeedConfig): (req: MedusaRequest, res: MedusaResponse) => Promise<void>;
package/dist/route.js ADDED
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createFeedRouteHandler = createFeedRouteHandler;
4
+ const utils_1 = require("@medusajs/framework/utils");
5
+ const feed_cache_1 = require("./feed-cache");
6
+ const feed_query_1 = require("./feed-query");
7
+ const feed_builder_1 = require("./feed-builder");
8
+ function createFeedRouteHandler(config) {
9
+ return async function GET(req, res) {
10
+ try {
11
+ const cacheService = req.scope.resolve(utils_1.Modules.CACHE);
12
+ const query = req.scope.resolve(utils_1.ContainerRegistrationKeys.QUERY);
13
+ let resolvedVariantMode = config.variantMode;
14
+ if (config.variantMode === "config") {
15
+ const qv = req.query["variants"];
16
+ if (typeof qv === "string" && (qv === "parent" || qv === "children")) {
17
+ resolvedVariantMode = qv;
18
+ }
19
+ else {
20
+ resolvedVariantMode = "children";
21
+ }
22
+ }
23
+ const cacheKey = `product-feed:${resolvedVariantMode}`;
24
+ const effectiveConfig = { ...config, variantMode: resolvedVariantMode };
25
+ if (!config.disableCache) {
26
+ const cached = await (0, feed_cache_1.getFeed)(cacheService, cacheKey);
27
+ if (cached) {
28
+ res.setHeader("Content-Type", "application/xml; charset=utf-8");
29
+ res.setHeader("X-Feed-Cache", "HIT");
30
+ res.send(cached);
31
+ return;
32
+ }
33
+ }
34
+ const products = await (0, feed_query_1.fetchFeedProducts)(query, config.currency, config.regionId);
35
+ const xml = (0, feed_builder_1.buildGoogleMerchantFeed)(products, effectiveConfig);
36
+ if (!config.disableCache) {
37
+ await (0, feed_cache_1.setFeed)(cacheService, xml, config.cacheTtlSeconds, cacheKey);
38
+ }
39
+ res.setHeader("Content-Type", "application/xml; charset=utf-8");
40
+ res.setHeader("X-Feed-Cache", config.disableCache ? "DISABLED" : "MISS");
41
+ res.send(xml);
42
+ }
43
+ catch (err) {
44
+ console.error("[medusa-product-feed] error:", err);
45
+ res.status(500).send("Feed generation failed");
46
+ }
47
+ };
48
+ }
@@ -0,0 +1,3 @@
1
+ import type { SubscriberArgs, SubscriberConfig } from "@medusajs/medusa";
2
+ export declare function productFeedInvalidationSubscriber({ container, }: SubscriberArgs<unknown>): Promise<void>;
3
+ export declare const config: SubscriberConfig;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.config = void 0;
4
+ exports.productFeedInvalidationSubscriber = productFeedInvalidationSubscriber;
5
+ const utils_1 = require("@medusajs/framework/utils");
6
+ const feed_cache_1 = require("./feed-cache");
7
+ async function productFeedInvalidationSubscriber({ container, }) {
8
+ const cacheService = container.resolve(utils_1.Modules.CACHE);
9
+ // Invalidate all possible cache keys (fixed key + per-mode keys)
10
+ await Promise.all([
11
+ (0, feed_cache_1.invalidateFeed)(cacheService),
12
+ (0, feed_cache_1.invalidateFeed)(cacheService, "product-feed:parent"),
13
+ (0, feed_cache_1.invalidateFeed)(cacheService, "product-feed:children"),
14
+ ]);
15
+ }
16
+ exports.config = {
17
+ event: [
18
+ "product.created",
19
+ "product.updated",
20
+ "product.deleted",
21
+ ],
22
+ };
@@ -0,0 +1,3 @@
1
+ import type { SubscriberArgs, SubscriberConfig } from "@medusajs/medusa";
2
+ export default function productFeedInvalidationSubscriber({ container, }: SubscriberArgs<unknown>): Promise<void>;
3
+ export declare const config: SubscriberConfig;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.config = void 0;
4
+ exports.default = productFeedInvalidationSubscriber;
5
+ const utils_1 = require("@medusajs/framework/utils");
6
+ const feed_cache_1 = require("../feed-cache");
7
+ async function productFeedInvalidationSubscriber({ container, }) {
8
+ const cacheService = container.resolve(utils_1.Modules.CACHE);
9
+ await Promise.all([
10
+ (0, feed_cache_1.invalidateFeed)(cacheService, "product-feed:parent"),
11
+ (0, feed_cache_1.invalidateFeed)(cacheService, "product-feed:children"),
12
+ ]);
13
+ }
14
+ exports.config = {
15
+ event: ["product.created", "product.updated", "product.deleted"],
16
+ };
@@ -0,0 +1,36 @@
1
+ export type VariantMode = "parent" | "children" | "config";
2
+ export interface FeedConfig {
3
+ storefrontUrl: string;
4
+ currency: string;
5
+ regionId: string;
6
+ variantMode: VariantMode;
7
+ cacheTtlSeconds: number;
8
+ disableCache?: boolean;
9
+ storeName?: string;
10
+ }
11
+ export interface FeedVariant {
12
+ id: string;
13
+ title: string | null;
14
+ sku: string | null;
15
+ prices: Array<{
16
+ currency_code: string;
17
+ amount: number;
18
+ }>;
19
+ manage_inventory: boolean;
20
+ inventory_quantity: number;
21
+ }
22
+ export interface FeedProduct {
23
+ id: string;
24
+ title: string;
25
+ handle: string;
26
+ description: string | null;
27
+ images: Array<{
28
+ url: string;
29
+ }>;
30
+ collection: {
31
+ title: string;
32
+ } | null;
33
+ variants: FeedVariant[];
34
+ metadata: Record<string, unknown> | null;
35
+ }
36
+ export declare function feedConfigFromEnv(): FeedConfig;
package/dist/types.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.feedConfigFromEnv = feedConfigFromEnv;
4
+ function feedConfigFromEnv() {
5
+ return {
6
+ storefrontUrl: process.env.FEED_STOREFRONT_URL ?? process.env.STOREFRONT_URL ?? "http://localhost:8000",
7
+ currency: process.env.FEED_CURRENCY ?? "ILS",
8
+ regionId: process.env.FEED_REGION_ID ?? "",
9
+ variantMode: (process.env.FEED_VARIANT_MODE ?? "children"),
10
+ cacheTtlSeconds: Number(process.env.FEED_CACHE_TTL ?? 3600),
11
+ disableCache: process.env.FEED_DISABLE_CACHE === "true",
12
+ storeName: process.env.FEED_STORE_NAME,
13
+ };
14
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "medusa-product-feed",
3
+ "version": "0.1.0",
4
+ "description": "Google Merchant Center XML product feed plugin for Medusa 2.x",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": ["dist"],
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "test": "jest",
17
+ "test:unit": "jest src"
18
+ },
19
+ "devDependencies": {
20
+ "@swc/core": "^1.5.7",
21
+ "@swc/jest": "^0.2.39",
22
+ "@types/jest": "^29.5.14",
23
+ "@types/node": "^20.19.25",
24
+ "jest": "^29.7.0",
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "peerDependencies": {
28
+ "@medusajs/framework": "^2.13.6",
29
+ "@medusajs/medusa": "^2.13.6"
30
+ }
31
+ }