@rudra-js/react 0.4.0 → 0.6.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 +21 -7
- package/dist/blocks/block-renderers.d.ts +1 -1
- package/dist/blocks/block-renderers.d.ts.map +1 -1
- package/dist/blocks/block-renderers.js +3 -2
- package/dist/blocks/block-renderers.js.map +1 -1
- package/dist/blocks/bundle-block.d.ts +1 -1
- package/dist/blocks/bundle-block.d.ts.map +1 -1
- package/dist/blocks/bundle-block.js +2 -1
- package/dist/blocks/bundle-block.js.map +1 -1
- package/dist/blocks/product-card.d.ts +1 -1
- package/dist/blocks/product-card.d.ts.map +1 -1
- package/dist/blocks/product-card.js +2 -1
- package/dist/blocks/product-card.js.map +1 -1
- package/dist/render-context.d.ts +10 -0
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +15 -16
- package/dist/render-context.js.map +1 -1
- package/dist/rudra-component.d.ts.map +1 -1
- package/dist/rudra-component.js +15 -45
- package/dist/rudra-component.js.map +1 -1
- package/package.json +7 -7
- package/src/blocks/block-renderers.tsx +3 -6
- package/src/blocks/bundle-block.tsx +2 -2
- package/src/blocks/product-card.tsx +2 -2
- package/src/render-context.ts +19 -16
- package/src/rudra-component.tsx +18 -47
package/README.md
CHANGED
|
@@ -51,9 +51,16 @@ resolved at render time from `products`, keyed by a SKU reconciliation has alrea
|
|
|
51
51
|
**Validate `products` with `productSchema` from `@rudra-js/core`**, the same schema your candidates
|
|
52
52
|
already passed. This prop is a second door into the framework. `imageUrl` lands in an `<img src>`,
|
|
53
53
|
and `productSchema` is what rejects a protocol-relative `//evil.example/pixel.png` or a `data:` URL.
|
|
54
|
-
|
|
54
|
+
It reads a path the way a browser does, so `/\evil.example/pixel.png` and the same trick written
|
|
55
|
+
with a tab or a newline in it are rejected too — each one resolves to someone else's host. React
|
|
56
|
+
neutralises a `javascript:` URL by itself, but not any of those. And if a price isn't a finite
|
|
55
57
|
number, it throws. A product that looks free is worse than a stack trace.
|
|
56
58
|
|
|
59
|
+
A row flagged `isInStock: false` counts as a row that isn't there. Cards for it are dropped, a hero
|
|
60
|
+
loses its link, and a bundle with one sold-out member disappears — the same as if you'd left the
|
|
61
|
+
row out. Core only ever names a product that was in stock in the payload, but this catalog is read
|
|
62
|
+
later and may be the fresher of the two.
|
|
63
|
+
|
|
57
64
|
## Styling
|
|
58
65
|
|
|
59
66
|
We ship no CSS. A stylesheet of ours would only fight whatever your site already has. So out of the
|
|
@@ -127,6 +134,11 @@ const registry = extendRegistry({
|
|
|
127
134
|
<RudraComponent spec={spec} products={catalog} registry={registry} />;
|
|
128
135
|
```
|
|
129
136
|
|
|
137
|
+
A grid, carousel or bundle block with nothing left in your catalog is dropped before any renderer
|
|
138
|
+
runs — yours as well as ours. The component has to decide whether to draw the wrapper and its
|
|
139
|
+
headline at all, and the only way to decide is to ask each block first. Hero, banner and copy always
|
|
140
|
+
reach the renderer, since they don't depend on the catalog.
|
|
141
|
+
|
|
130
142
|
## Props
|
|
131
143
|
|
|
132
144
|
| Prop | Notes |
|
|
@@ -157,9 +169,10 @@ That's a `Set` of products, a plain object, or a `Map` that has been through JSO
|
|
|
157
169
|
error than a quietly empty recommendation area.
|
|
158
170
|
|
|
159
171
|
When there's nothing left to show, the component renders nothing at all. That covers a spec with no
|
|
160
|
-
blocks, and one whose every product has left your catalog since it was generated.
|
|
161
|
-
|
|
162
|
-
|
|
172
|
+
blocks, and one whose every product has left your catalog since it was generated. A single block
|
|
173
|
+
goes the same way on its own: it drops out and the rest of the component carries on without it. An
|
|
174
|
+
empty recommendation area, or a headline over an empty box, takes up space and tells the shopper the
|
|
175
|
+
page is broken.
|
|
163
176
|
|
|
164
177
|
### What `bundles` is
|
|
165
178
|
|
|
@@ -174,9 +187,10 @@ in, and your name for it, so the component has something to draw.
|
|
|
174
187
|
|
|
175
188
|
**Validate `bundles` with `bundleSchema` from `@rudra-js/core`, and pass the same list you sent to
|
|
176
189
|
`parseTrackingInput`.** Core already checked that list — every member in stock, none of them
|
|
177
|
-
disliked, no repeats, and the whole set inside the item
|
|
178
|
-
but the id it chose. Pass a stale or different list
|
|
179
|
-
ever saw, under an id that was proved against
|
|
190
|
+
disliked, none of them the product being looked at, no repeats, and the whole set inside the item
|
|
191
|
+
budget — and then hands the renderer nothing but the id it chose. Pass a stale or different list
|
|
192
|
+
here and you'll draw a set none of those checks ever saw, under an id that was proved against
|
|
193
|
+
another one.
|
|
180
194
|
|
|
181
195
|
## Licence
|
|
182
196
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* and wording.
|
|
9
9
|
*/
|
|
10
10
|
import type { BannerBlock, CarouselBlock, CopyBlock, GridBlock, HeroBlock } from '@rudra-js/core';
|
|
11
|
-
import type
|
|
11
|
+
import { type BlockRenderContext } from '../render-context.js';
|
|
12
12
|
export declare function HeroRenderer({ block, context, }: {
|
|
13
13
|
block: HeroBlock;
|
|
14
14
|
context: BlockRenderContext;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-renderers.d.ts","sourceRoot":"","sources":["../../src/blocks/block-renderers.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAClG,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"block-renderers.d.ts","sourceRoot":"","sources":["../../src/blocks/block-renderers.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAClG,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAGhF,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,+BAoBA;AAED,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,+BAWA;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,+BAaA;AAED,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,WAAW,CAAA;CAAE,+BAO/D;AAED,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,+BAO3D"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { sellableProduct } from '../render-context.js';
|
|
2
3
|
import { ProductCard } from './product-card.js';
|
|
3
4
|
export function HeroRenderer({ block, context, }) {
|
|
4
|
-
const product = block.sku ? context.products
|
|
5
|
-
return (_jsxs("section", { className: "rudra-hero", children: [_jsx("h3", { className: "rudra-hero__headline", children: block.headline }), block.body ? _jsx("p", { className: "rudra-hero__body", children: block.body }) : null, product ? (_jsxs("a", { className: "rudra-hero__link", href: context.hrefForSku(product.sku), "data-rudra-sku": product.sku, children: [product.title, _jsx("span", { className: "rudra-hero__price", children: context.formatPrice(product) })
|
|
5
|
+
const product = block.sku ? sellableProduct(context.products, block.sku) : undefined;
|
|
6
|
+
return (_jsxs("section", { className: "rudra-hero", children: [_jsx("h3", { className: "rudra-hero__headline", children: block.headline }), block.body ? _jsx("p", { className: "rudra-hero__body", children: block.body }) : null, product ? (_jsxs("a", { className: "rudra-hero__link", href: context.hrefForSku(product.sku), "data-rudra-sku": product.sku, children: [product.title, _jsx("span", { className: "rudra-hero__price", children: context.formatPrice(product) }), block.ctaLabel ? _jsx("span", { className: "rudra-hero__cta", children: block.ctaLabel }) : null] })) : null] }));
|
|
6
7
|
}
|
|
7
8
|
export function GridRenderer({ block, context, }) {
|
|
8
9
|
return (_jsxs("section", { className: "rudra-grid", "data-rudra-columns": block.columns, children: [block.title ? _jsx("h3", { className: "rudra-grid__title", children: block.title }) : null, _jsx("div", { className: "rudra-grid__items", children: block.items.map((reference) => (_jsx(ProductCard, { reference: reference, context: context }, reference.sku))) })] }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-renderers.js","sourceRoot":"","sources":["../../src/blocks/block-renderers.tsx"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"block-renderers.js","sourceRoot":"","sources":["../../src/blocks/block-renderers.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BundleBlock } from '@rudra-js/core';
|
|
2
|
-
import type
|
|
2
|
+
import { type BlockRenderContext } from '../render-context.js';
|
|
3
3
|
export declare function BundleRenderer({ block, context, }: {
|
|
4
4
|
block: BundleBlock;
|
|
5
5
|
context: BlockRenderContext;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle-block.d.ts","sourceRoot":"","sources":["../../src/blocks/bundle-block.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAW,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"bundle-block.d.ts","sourceRoot":"","sources":["../../src/blocks/bundle-block.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAW,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAEhF,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,sCAiCA"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { sellableProduct } from '../render-context.js';
|
|
2
3
|
export function BundleRenderer({ block, context, }) {
|
|
3
4
|
const bundle = block.bundleId === null ? undefined : context.bundles.get(block.bundleId);
|
|
4
5
|
if (!bundle)
|
|
5
6
|
return null;
|
|
6
7
|
const products = [];
|
|
7
8
|
for (const sku of bundle.skus) {
|
|
8
|
-
const product = context.products
|
|
9
|
+
const product = sellableProduct(context.products, sku);
|
|
9
10
|
// A set missing one of its parts is not that set.
|
|
10
11
|
if (!product)
|
|
11
12
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle-block.js","sourceRoot":"","sources":["../../src/blocks/bundle-block.tsx"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"bundle-block.js","sourceRoot":"","sources":["../../src/blocks/bundle-block.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAA2B,MAAM,sBAAsB,CAAC;AAEhF,MAAM,UAAU,cAAc,CAAC,EAC7B,KAAK,EACL,OAAO,GAIR;IACC,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACzF,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,eAAe,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QACvD,kDAAkD;QAClD,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC;QAC1B,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,CACL,mBAAS,SAAS,EAAC,cAAc,aAE9B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,aAAI,SAAS,EAAC,qBAAqB,YAAE,MAAM,CAAC,KAAK,GAAM,CAAC,CAAC,CAAC,IAAI,EAC7E,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,YAAG,SAAS,EAAC,qBAAqB,YAAE,KAAK,CAAC,KAAK,GAAK,CAAC,CAAC,CAAC,IAAI,EACzE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,YAAG,SAAS,EAAC,oBAAoB,YAAE,KAAK,CAAC,IAAI,GAAK,CAAC,CAAC,CAAC,IAAI,EAEvE,aAAI,SAAS,EAAC,qBAAqB,YAChC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CACzB,aAAsB,SAAS,EAAC,oBAAoB,oBAAiB,OAAO,CAAC,GAAG,YAC9E,YAAG,SAAS,EAAC,oBAAoB,EAAC,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,YACpE,OAAO,CAAC,KAAK,GACZ,IAHG,OAAO,CAAC,GAAG,CAIf,CACN,CAAC,GACC,EAEL,YAAG,SAAS,EAAC,qBAAqB,YAAE,OAAO,CAAC,iBAAiB,CAAC,MAAM,CAAC,GAAK,EACzE,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,mBAAmB,YAAE,KAAK,CAAC,QAAQ,GAAQ,CAAC,CAAC,CAAC,IAAI,IAC5E,CACX,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"product-card.d.ts","sourceRoot":"","sources":["../../src/blocks/product-card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"product-card.d.ts","sourceRoot":"","sources":["../../src/blocks/product-card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAEhF;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EACT,OAAO,GACR,EAAE;IACD,SAAS,EAAE,gBAAgB,CAAC;IAC5B,OAAO,EAAE,kBAAkB,CAAC;CAC7B,sCA2BA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { sellableProduct } from '../render-context.js';
|
|
2
3
|
/**
|
|
3
4
|
* One product.
|
|
4
5
|
*
|
|
@@ -8,7 +9,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
9
|
* into the markup.
|
|
9
10
|
*/
|
|
10
11
|
export function ProductCard({ reference, context, }) {
|
|
11
|
-
const product = context.products
|
|
12
|
+
const product = sellableProduct(context.products, reference.sku);
|
|
12
13
|
// Reconciliation drops a SKU the catalog does not have, so this should be
|
|
13
14
|
// unreachable. Rendering nothing beats rendering a card with holes in it.
|
|
14
15
|
if (!product)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"product-card.js","sourceRoot":"","sources":["../../src/blocks/product-card.tsx"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"product-card.js","sourceRoot":"","sources":["../../src/blocks/product-card.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAA2B,MAAM,sBAAsB,CAAC;AAEhF;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,SAAS,EACT,OAAO,GAIR;IACC,MAAM,OAAO,GAAG,eAAe,CAAC,OAAO,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IACjE,0EAA0E;IAC1E,0EAA0E;IAC1E,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,MAAM,UAAU,GAAG,SAAS,CAAC,QAAQ,KAAK,UAAU,CAAC;IAErD,OAAO,CACL,aACE,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,oBACrB,OAAO,CAAC,GAAG,sBACT,SAAS,CAAC,KAAK,EACjC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC,iCAAiC,CAAC,CAAC,CAAC,YAAY,aAEvE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAClB,cAAK,SAAS,EAAC,mBAAmB,EAAC,GAAG,EAAE,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAC,EAAE,EAAC,OAAO,EAAC,MAAM,GAAG,CACnF,CAAC,CAAC,CAAC,IAAI,EAER,gBAAM,SAAS,EAAC,kBAAkB,aAC/B,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,mBAAmB,YAAE,SAAS,CAAC,KAAK,GAAQ,CAAC,CAAC,CAAC,IAAI,EACtF,eAAM,SAAS,EAAC,mBAAmB,YAAE,OAAO,CAAC,KAAK,GAAQ,EAC1D,eAAM,SAAS,EAAC,mBAAmB,YAAE,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,GAAQ,EACxE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,oBAAoB,YAAE,SAAS,CAAC,MAAM,GAAQ,CAAC,CAAC,CAAC,IAAI,IACpF,IACL,CACL,CAAC;AACJ,CAAC"}
|
package/dist/render-context.d.ts
CHANGED
|
@@ -22,6 +22,16 @@ export interface BlockRenderContext {
|
|
|
22
22
|
readonly formatBundlePrice: (bundle: Bundle) => string;
|
|
23
23
|
}
|
|
24
24
|
export declare function defaultHrefForSku(sku: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* The product to draw for a SKU, or nothing.
|
|
27
|
+
*
|
|
28
|
+
* A row the catalog flags out of stock counts as absent. Core only ever names
|
|
29
|
+
* an in-stock candidate, but the catalog passed here is read later and can be
|
|
30
|
+
* the fresher of the two — and a card that prices and links something the shop
|
|
31
|
+
* cannot sell is worse than one card fewer. A catalog that leaves the field
|
|
32
|
+
* off renders as it always did.
|
|
33
|
+
*/
|
|
34
|
+
export declare function sellableProduct(products: ReadonlyMap<string, Product>, sku: string): Product | undefined;
|
|
25
35
|
/**
|
|
26
36
|
* Formats a price the way the currency itself is written.
|
|
27
37
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-context.d.ts","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChD,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,oCAAoC;IACpC,QAAQ,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IACnD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CACxD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"render-context.d.ts","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChD,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,oCAAoC;IACpC,QAAQ,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IACnD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CACxD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,GAAG,EAAE,MAAM,GACV,OAAO,GAAG,SAAS,CAGrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAkB5E;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAgBhF"}
|
package/dist/render-context.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
export function defaultHrefForSku(sku) {
|
|
2
2
|
return `/product/${encodeURIComponent(sku)}`;
|
|
3
3
|
}
|
|
4
|
+
/**
|
|
5
|
+
* The product to draw for a SKU, or nothing.
|
|
6
|
+
*
|
|
7
|
+
* A row the catalog flags out of stock counts as absent. Core only ever names
|
|
8
|
+
* an in-stock candidate, but the catalog passed here is read later and can be
|
|
9
|
+
* the fresher of the two — and a card that prices and links something the shop
|
|
10
|
+
* cannot sell is worse than one card fewer. A catalog that leaves the field
|
|
11
|
+
* off renders as it always did.
|
|
12
|
+
*/
|
|
13
|
+
export function sellableProduct(products, sku) {
|
|
14
|
+
const product = products.get(sku);
|
|
15
|
+
return product?.isInStock === false ? undefined : product;
|
|
16
|
+
}
|
|
4
17
|
/**
|
|
5
18
|
* Formats a price the way the currency itself is written.
|
|
6
19
|
*
|
|
@@ -15,29 +28,17 @@ export function defaultHrefForSku(sku) {
|
|
|
15
28
|
* the shopper's — a shop serving more than one should pass the shopper's.
|
|
16
29
|
*/
|
|
17
30
|
export function defaultFormatPrice(product, locale) {
|
|
18
|
-
// A price that is not a finite number is a broken catalog, not a formatting
|
|
19
|
-
// problem, and Intl will happily render it: null becomes 0 and prints as a
|
|
20
|
-
// free product, undefined prints as NaN. Neither belongs on a shop page, and
|
|
21
|
-
// both mean the catalog skipped the validation this package documents as a
|
|
22
|
-
// precondition. Failing here is a bug report; rendering it is an incident.
|
|
23
31
|
if (!Number.isFinite(product.price)) {
|
|
24
32
|
throw new TypeError(`price for SKU ${product.sku} is ${String(product.price)}, not a finite number — ` +
|
|
25
33
|
'catalog objects must satisfy productSchema from @rudra-js/core');
|
|
26
34
|
}
|
|
27
35
|
let formatter;
|
|
28
36
|
try {
|
|
29
|
-
// Only the constructor
|
|
30
|
-
//
|
|
31
|
-
// mess as a price.
|
|
37
|
+
// Only the constructor: guarding format() too would swallow a throwing
|
|
38
|
+
// getter on a host's own Product and render the mess as a price.
|
|
32
39
|
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: product.currency });
|
|
33
40
|
}
|
|
34
41
|
catch {
|
|
35
|
-
// Two things reach this. A malformed `locale`, which is a host prop nothing
|
|
36
|
-
// validates. And a malformed `currency` — which productSchema would have
|
|
37
|
-
// rejected, but this package takes a catalog directly, so a hand-built
|
|
38
|
-
// object can carry one. Intl accepts any three-letter code it does not
|
|
39
|
-
// know, so 'ZZZ' formats; 'us$' and '' do not. A price nobody can
|
|
40
|
-
// punctuate is still a price worth showing.
|
|
41
42
|
return `${product.currency} ${product.price}`;
|
|
42
43
|
}
|
|
43
44
|
return formatter.format(product.price);
|
|
@@ -58,8 +59,6 @@ export function defaultFormatBundlePrice(bundle, locale) {
|
|
|
58
59
|
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: bundle.currency });
|
|
59
60
|
}
|
|
60
61
|
catch {
|
|
61
|
-
// A bad locale, or a currency code Intl rejects on a hand-built bundle,
|
|
62
|
-
// should not crash the page. A price nobody can punctuate is still a price.
|
|
63
62
|
return `${bundle.currency} ${bundle.price}`;
|
|
64
63
|
}
|
|
65
64
|
return formatter.format(bundle.price);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-context.js","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAyBA,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,YAAY,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"render-context.js","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAyBA,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,YAAY,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAsC,EACtC,GAAW;IAEX,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAClC,OAAO,OAAO,EAAE,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,MAAe;IAClE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,SAAS,CACjB,iBAAiB,OAAO,CAAC,GAAG,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,0BAA0B;YAChF,gEAAgE,CACnE,CAAC;IACJ,CAAC;IAED,IAAI,SAA4B,CAAC;IACjC,IAAI,CAAC;QACH,uEAAuE;QACvE,iEAAiE;QACjE,SAAS,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC/F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;IAChD,CAAC;IAED,OAAO,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAAc,EAAE,MAAe;IACtE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,SAAS,CACjB,oBAAoB,MAAM,CAAC,EAAE,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,0BAA0B;YAChF,8DAA8D,CACjE,CAAC;IACJ,CAAC;IAED,IAAI,SAA4B,CAAC;IACjC,IAAI,CAAC;QACH,SAAS,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC9F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;IAC9C,CAAC;IAED,OAAO,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rudra-component.d.ts","sourceRoot":"","sources":["../src/rudra-component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"rudra-component.d.ts","sourceRoot":"","sources":["../src/rudra-component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAQ5E,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAEpE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,aAAa,CAAC;IACpB;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,EAAE,cAAc,CAAC;IACzB,oFAAoF;IACpF,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IAC3C,0FAA0F;IAC1F,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/C;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,iFAAiF;AACjF,MAAM,MAAM,cAAc,GAAG,SAAS,OAAO,EAAE,GAAG,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAiF/E;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,QAA0B,EAC1B,UAA8B,EAC9B,WAAW,EACX,iBAAiB,EACjB,MAAM,EACN,cAAsB,EACtB,SAAS,GACV,EAAE,mBAAmB,sCA8CrB"}
|
package/dist/rudra-component.js
CHANGED
|
@@ -1,21 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { defaultFormatBundlePrice, defaultFormatPrice, defaultHrefForSku, } from './render-context.js';
|
|
2
|
+
import { defaultFormatBundlePrice, defaultFormatPrice, defaultHrefForSku, sellableProduct, } from './render-context.js';
|
|
3
3
|
import { defaultRegistry } from './registry.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* happened to construct. `instanceof Map` was wrong twice over: a Map that
|
|
9
|
-
* crossed a realm boundary — a `node:vm` context, a worker — fails it, and so
|
|
10
|
-
* does a host's own `ReadonlyMap`, which the prop type has always allowed. Both
|
|
11
|
-
* then fell into the list branch, where `catalog.map is not a function` throws
|
|
12
|
-
* while the render context is being built, before any block renders. That takes
|
|
13
|
-
* down the whole page, not just this component.
|
|
14
|
-
*
|
|
15
|
-
* Keyed before list, because a collection can answer both: an Immutable.js map
|
|
16
|
-
* has `map`, and converting through it yields a catalog whose every value is a
|
|
17
|
-
* `[sku, product]` pair rather than a product.
|
|
18
|
-
*/
|
|
4
|
+
// Asks what the renderers call rather than which class the host built, since
|
|
5
|
+
// `instanceof Map` is per-realm. Checked before the list branch because a
|
|
6
|
+
// collection can answer both: an Immutable.js map has `map`, and converting
|
|
7
|
+
// through it yields `[sku, product]` pairs rather than products.
|
|
19
8
|
function isKeyedBySku(catalog) {
|
|
20
9
|
const candidate = catalog;
|
|
21
10
|
return typeof candidate.get === 'function' && typeof candidate.has === 'function';
|
|
@@ -24,29 +13,20 @@ function toProductMap(catalog) {
|
|
|
24
13
|
if (isKeyedBySku(catalog))
|
|
25
14
|
return catalog;
|
|
26
15
|
if (typeof catalog.map !== 'function') {
|
|
27
|
-
// A Set of products, a plain object keyed by SKU, a Map that has been
|
|
28
|
-
// through JSON. Refusing here names the prop while the stack still points
|
|
29
|
-
// at it. Carried through instead, a grid renders nothing and a banner
|
|
30
|
-
// renders a healthy-looking page, and the shop finds out from a dashboard.
|
|
31
16
|
throw new TypeError('the `products` prop must be a list of products, or keyed by SKU with `get` and `has` — ' +
|
|
32
17
|
`received ${Object.prototype.toString.call(catalog)}`);
|
|
33
18
|
}
|
|
34
19
|
return new Map(catalog.map((product) => [product.sku, product]));
|
|
35
20
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
* as it was when the spec was generated, and a SKU can sell out between then
|
|
41
|
-
* and this render. Grid and carousel lose just the products that did; a
|
|
42
|
-
* bundle loses itself entirely if any one of its members did. The rest carry
|
|
43
|
-
* their own words.
|
|
44
|
-
*/
|
|
21
|
+
// Whether a block still has anything to say once the catalog is applied. A SKU
|
|
22
|
+
// can sell out between generating a spec and rendering it — the row goes, or it
|
|
23
|
+
// is flagged out of stock, and both read the same here: grid and carousel lose
|
|
24
|
+
// the products that did, a bundle loses itself if any one member did.
|
|
45
25
|
function hasContent(block, products, bundles) {
|
|
46
26
|
switch (block.kind) {
|
|
47
27
|
case 'grid':
|
|
48
28
|
case 'carousel':
|
|
49
|
-
return block.items.some((reference) => products
|
|
29
|
+
return block.items.some((reference) => sellableProduct(products, reference.sku) !== undefined);
|
|
50
30
|
case 'hero':
|
|
51
31
|
case 'banner':
|
|
52
32
|
case 'copy':
|
|
@@ -55,10 +35,10 @@ function hasContent(block, products, bundles) {
|
|
|
55
35
|
if (block.bundleId === null)
|
|
56
36
|
return false;
|
|
57
37
|
const bundle = bundles.get(block.bundleId);
|
|
58
|
-
return bundle !== undefined &&
|
|
38
|
+
return (bundle !== undefined &&
|
|
39
|
+
bundle.skus.every((sku) => sellableProduct(products, sku) !== undefined));
|
|
59
40
|
}
|
|
60
41
|
default:
|
|
61
|
-
// A kind this renderer predates renders nothing, so it counts as nothing.
|
|
62
42
|
block;
|
|
63
43
|
return false;
|
|
64
44
|
}
|
|
@@ -78,9 +58,7 @@ function renderBlock(block, context, registry, index) {
|
|
|
78
58
|
case 'bundle':
|
|
79
59
|
return _jsx(registry.bundle, { block: block, context: context }, index);
|
|
80
60
|
default:
|
|
81
|
-
// A
|
|
82
|
-
// block rather than the page. In this repo the assertion below fails the
|
|
83
|
-
// build instead, which is the moment it is cheap to notice.
|
|
61
|
+
// A block kind this renderer predates loses that block, not the page.
|
|
84
62
|
block;
|
|
85
63
|
return null;
|
|
86
64
|
}
|
|
@@ -106,15 +84,10 @@ export function RudraComponent({ spec, products, bundles, registry = defaultRegi
|
|
|
106
84
|
formatPrice: formatPrice ?? ((product) => defaultFormatPrice(product, locale)),
|
|
107
85
|
formatBundlePrice: formatBundlePrice ?? ((bundle) => defaultFormatBundlePrice(bundle, locale)),
|
|
108
86
|
};
|
|
109
|
-
// An empty recommendation area is worse than none: it takes up space and
|
|
110
|
-
// tells the shopper the page is broken. That includes the subtler version —
|
|
111
|
-
// a headline and an empty box, because every product in the spec has sold out
|
|
112
|
-
// since it was generated — which is why this asks what is left rather than
|
|
113
|
-
// how many blocks arrived.
|
|
114
87
|
const visible = spec.blocks.filter((block) => hasContent(block, context.products, context.bundles));
|
|
115
88
|
if (visible.length === 0)
|
|
116
89
|
return null;
|
|
117
|
-
// React
|
|
90
|
+
// React drops a data-* attribute whose value is undefined, so degradedReason
|
|
118
91
|
// needs no branch of its own.
|
|
119
92
|
const diagnosticAttributes = hasDiagnostics
|
|
120
93
|
? {
|
|
@@ -124,9 +97,6 @@ export function RudraComponent({ spec, products, bundles, registry = defaultRegi
|
|
|
124
97
|
'data-rudra-degraded': spec.degradedReason,
|
|
125
98
|
}
|
|
126
99
|
: undefined;
|
|
127
|
-
return (_jsxs("section", {
|
|
128
|
-
// Extended rather than replaced: every child class is namespaced under
|
|
129
|
-
// `rudra`, and the package ships no stylesheet, so a host will pass one.
|
|
130
|
-
className: className ? `rudra ${className}` : 'rudra', "data-rudra-slot": spec.slot, "data-rudra-source": spec.source, "data-rudra-tone": spec.tone, ...diagnosticAttributes, children: [_jsxs("header", { className: "rudra__header", children: [_jsx("h2", { className: "rudra__headline", children: spec.headline }), spec.subheadline ? _jsx("p", { className: "rudra__subheadline", children: spec.subheadline }) : null] }), visible.map((block, index) => renderBlock(block, context, registry, index)), hasDiagnostics ? _jsx("p", { className: "rudra__rationale", children: spec.rationale }) : null] }));
|
|
100
|
+
return (_jsxs("section", { className: className ? `rudra ${className}` : 'rudra', "data-rudra-slot": spec.slot, "data-rudra-source": spec.source, "data-rudra-tone": spec.tone, ...diagnosticAttributes, children: [_jsxs("header", { className: "rudra__header", children: [_jsx("h2", { className: "rudra__headline", children: spec.headline }), spec.subheadline ? _jsx("p", { className: "rudra__subheadline", children: spec.subheadline }) : null] }), visible.map((block, index) => renderBlock(block, context, registry, index)), hasDiagnostics ? _jsx("p", { className: "rudra__rationale", children: spec.rationale }) : null] }));
|
|
131
101
|
}
|
|
132
102
|
//# sourceMappingURL=rudra-component.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rudra-component.js","sourceRoot":"","sources":["../src/rudra-component.tsx"],"names":[],"mappings":";AACA,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"rudra-component.js","sourceRoot":"","sources":["../src/rudra-component.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rudra-js/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Renders a rudra component specification as React Server Components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -51,14 +51,14 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@rudra-js/core": "^0.
|
|
54
|
+
"@rudra-js/core": "^0.6.0",
|
|
55
55
|
"react": "^18.2.0 || ^19.0.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@rudra-js/core": "0.
|
|
59
|
-
"@types/react": "^19.
|
|
60
|
-
"@types/react-dom": "^19.
|
|
61
|
-
"react": "^19.
|
|
62
|
-
"react-dom": "^19.
|
|
58
|
+
"@rudra-js/core": "0.6.0",
|
|
59
|
+
"@types/react": "^19.3.0",
|
|
60
|
+
"@types/react-dom": "^19.3.0",
|
|
61
|
+
"react": "^19.3.0",
|
|
62
|
+
"react-dom": "^19.3.0"
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { BannerBlock, CarouselBlock, CopyBlock, GridBlock, HeroBlock } from '@rudra-js/core';
|
|
12
|
-
import type
|
|
12
|
+
import { sellableProduct, type BlockRenderContext } from '../render-context.js';
|
|
13
13
|
import { ProductCard } from './product-card.js';
|
|
14
14
|
|
|
15
15
|
export function HeroRenderer({
|
|
@@ -19,7 +19,7 @@ export function HeroRenderer({
|
|
|
19
19
|
block: HeroBlock;
|
|
20
20
|
context: BlockRenderContext;
|
|
21
21
|
}) {
|
|
22
|
-
const product = block.sku ? context.products
|
|
22
|
+
const product = block.sku ? sellableProduct(context.products, block.sku) : undefined;
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
25
|
<section className="rudra-hero">
|
|
@@ -33,12 +33,9 @@ export function HeroRenderer({
|
|
|
33
33
|
>
|
|
34
34
|
{product.title}
|
|
35
35
|
<span className="rudra-hero__price">{context.formatPrice(product)}</span>
|
|
36
|
+
{block.ctaLabel ? <span className="rudra-hero__cta">{block.ctaLabel}</span> : null}
|
|
36
37
|
</a>
|
|
37
38
|
) : null}
|
|
38
|
-
{/* Only alongside a product: a "Shop now" with no destination is worse
|
|
39
|
-
than no call to action, and the model can set one for a SKU the
|
|
40
|
-
catalog does not have. */}
|
|
41
|
-
{product && block.ctaLabel ? <span className="rudra-hero__cta">{block.ctaLabel}</span> : null}
|
|
42
39
|
</section>
|
|
43
40
|
);
|
|
44
41
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BundleBlock, Product } from '@rudra-js/core';
|
|
2
|
-
import type
|
|
2
|
+
import { sellableProduct, type BlockRenderContext } from '../render-context.js';
|
|
3
3
|
|
|
4
4
|
export function BundleRenderer({
|
|
5
5
|
block,
|
|
@@ -13,7 +13,7 @@ export function BundleRenderer({
|
|
|
13
13
|
|
|
14
14
|
const products: Product[] = [];
|
|
15
15
|
for (const sku of bundle.skus) {
|
|
16
|
-
const product = context.products
|
|
16
|
+
const product = sellableProduct(context.products, sku);
|
|
17
17
|
// A set missing one of its parts is not that set.
|
|
18
18
|
if (!product) return null;
|
|
19
19
|
products.push(product);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProductReference } from '@rudra-js/core';
|
|
2
|
-
import type
|
|
2
|
+
import { sellableProduct, type BlockRenderContext } from '../render-context.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* One product.
|
|
@@ -16,7 +16,7 @@ export function ProductCard({
|
|
|
16
16
|
reference: ProductReference;
|
|
17
17
|
context: BlockRenderContext;
|
|
18
18
|
}) {
|
|
19
|
-
const product = context.products
|
|
19
|
+
const product = sellableProduct(context.products, reference.sku);
|
|
20
20
|
// Reconciliation drops a SKU the catalog does not have, so this should be
|
|
21
21
|
// unreachable. Rendering nothing beats rendering a card with holes in it.
|
|
22
22
|
if (!product) return null;
|
package/src/render-context.ts
CHANGED
|
@@ -27,6 +27,23 @@ export function defaultHrefForSku(sku: string): string {
|
|
|
27
27
|
return `/product/${encodeURIComponent(sku)}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The product to draw for a SKU, or nothing.
|
|
32
|
+
*
|
|
33
|
+
* A row the catalog flags out of stock counts as absent. Core only ever names
|
|
34
|
+
* an in-stock candidate, but the catalog passed here is read later and can be
|
|
35
|
+
* the fresher of the two — and a card that prices and links something the shop
|
|
36
|
+
* cannot sell is worse than one card fewer. A catalog that leaves the field
|
|
37
|
+
* off renders as it always did.
|
|
38
|
+
*/
|
|
39
|
+
export function sellableProduct(
|
|
40
|
+
products: ReadonlyMap<string, Product>,
|
|
41
|
+
sku: string,
|
|
42
|
+
): Product | undefined {
|
|
43
|
+
const product = products.get(sku);
|
|
44
|
+
return product?.isInStock === false ? undefined : product;
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
/**
|
|
31
48
|
* Formats a price the way the currency itself is written.
|
|
32
49
|
*
|
|
@@ -41,11 +58,6 @@ export function defaultHrefForSku(sku: string): string {
|
|
|
41
58
|
* the shopper's — a shop serving more than one should pass the shopper's.
|
|
42
59
|
*/
|
|
43
60
|
export function defaultFormatPrice(product: Product, locale?: string): string {
|
|
44
|
-
// A price that is not a finite number is a broken catalog, not a formatting
|
|
45
|
-
// problem, and Intl will happily render it: null becomes 0 and prints as a
|
|
46
|
-
// free product, undefined prints as NaN. Neither belongs on a shop page, and
|
|
47
|
-
// both mean the catalog skipped the validation this package documents as a
|
|
48
|
-
// precondition. Failing here is a bug report; rendering it is an incident.
|
|
49
61
|
if (!Number.isFinite(product.price)) {
|
|
50
62
|
throw new TypeError(
|
|
51
63
|
`price for SKU ${product.sku} is ${String(product.price)}, not a finite number — ` +
|
|
@@ -55,17 +67,10 @@ export function defaultFormatPrice(product: Product, locale?: string): string {
|
|
|
55
67
|
|
|
56
68
|
let formatter: Intl.NumberFormat;
|
|
57
69
|
try {
|
|
58
|
-
// Only the constructor
|
|
59
|
-
//
|
|
60
|
-
// mess as a price.
|
|
70
|
+
// Only the constructor: guarding format() too would swallow a throwing
|
|
71
|
+
// getter on a host's own Product and render the mess as a price.
|
|
61
72
|
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: product.currency });
|
|
62
73
|
} catch {
|
|
63
|
-
// Two things reach this. A malformed `locale`, which is a host prop nothing
|
|
64
|
-
// validates. And a malformed `currency` — which productSchema would have
|
|
65
|
-
// rejected, but this package takes a catalog directly, so a hand-built
|
|
66
|
-
// object can carry one. Intl accepts any three-letter code it does not
|
|
67
|
-
// know, so 'ZZZ' formats; 'us$' and '' do not. A price nobody can
|
|
68
|
-
// punctuate is still a price worth showing.
|
|
69
74
|
return `${product.currency} ${product.price}`;
|
|
70
75
|
}
|
|
71
76
|
|
|
@@ -90,8 +95,6 @@ export function defaultFormatBundlePrice(bundle: Bundle, locale?: string): strin
|
|
|
90
95
|
try {
|
|
91
96
|
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: bundle.currency });
|
|
92
97
|
} catch {
|
|
93
|
-
// A bad locale, or a currency code Intl rejects on a hand-built bundle,
|
|
94
|
-
// should not crash the page. A price nobody can punctuate is still a price.
|
|
95
98
|
return `${bundle.currency} ${bundle.price}`;
|
|
96
99
|
}
|
|
97
100
|
|
package/src/rudra-component.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
defaultFormatBundlePrice,
|
|
4
4
|
defaultFormatPrice,
|
|
5
5
|
defaultHrefForSku,
|
|
6
|
+
sellableProduct,
|
|
6
7
|
type BlockRenderContext,
|
|
7
8
|
} from './render-context.js';
|
|
8
9
|
import { defaultRegistry, type BlockRegistry } from './registry.js';
|
|
@@ -54,21 +55,10 @@ export interface RudraComponentProps {
|
|
|
54
55
|
/** A list of products, or anything keyed by SKU that answers `get` and `has`. */
|
|
55
56
|
export type ProductCatalog = readonly Product[] | ReadonlyMap<string, Product>;
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* happened to construct. `instanceof Map` was wrong twice over: a Map that
|
|
62
|
-
* crossed a realm boundary — a `node:vm` context, a worker — fails it, and so
|
|
63
|
-
* does a host's own `ReadonlyMap`, which the prop type has always allowed. Both
|
|
64
|
-
* then fell into the list branch, where `catalog.map is not a function` throws
|
|
65
|
-
* while the render context is being built, before any block renders. That takes
|
|
66
|
-
* down the whole page, not just this component.
|
|
67
|
-
*
|
|
68
|
-
* Keyed before list, because a collection can answer both: an Immutable.js map
|
|
69
|
-
* has `map`, and converting through it yields a catalog whose every value is a
|
|
70
|
-
* `[sku, product]` pair rather than a product.
|
|
71
|
-
*/
|
|
58
|
+
// Asks what the renderers call rather than which class the host built, since
|
|
59
|
+
// `instanceof Map` is per-realm. Checked before the list branch because a
|
|
60
|
+
// collection can answer both: an Immutable.js map has `map`, and converting
|
|
61
|
+
// through it yields `[sku, product]` pairs rather than products.
|
|
72
62
|
function isKeyedBySku(catalog: ProductCatalog): catalog is ReadonlyMap<string, Product> {
|
|
73
63
|
const candidate = catalog as { get?: unknown; has?: unknown };
|
|
74
64
|
return typeof candidate.get === 'function' && typeof candidate.has === 'function';
|
|
@@ -77,10 +67,6 @@ function isKeyedBySku(catalog: ProductCatalog): catalog is ReadonlyMap<string, P
|
|
|
77
67
|
function toProductMap(catalog: ProductCatalog): ReadonlyMap<string, Product> {
|
|
78
68
|
if (isKeyedBySku(catalog)) return catalog;
|
|
79
69
|
if (typeof (catalog as { map?: unknown }).map !== 'function') {
|
|
80
|
-
// A Set of products, a plain object keyed by SKU, a Map that has been
|
|
81
|
-
// through JSON. Refusing here names the prop while the stack still points
|
|
82
|
-
// at it. Carried through instead, a grid renders nothing and a banner
|
|
83
|
-
// renders a healthy-looking page, and the shop finds out from a dashboard.
|
|
84
70
|
throw new TypeError(
|
|
85
71
|
'the `products` prop must be a list of products, or keyed by SKU with `get` and `has` — ' +
|
|
86
72
|
`received ${Object.prototype.toString.call(catalog)}`,
|
|
@@ -89,15 +75,10 @@ function toProductMap(catalog: ProductCatalog): ReadonlyMap<string, Product> {
|
|
|
89
75
|
return new Map(catalog.map((product) => [product.sku, product]));
|
|
90
76
|
}
|
|
91
77
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
* as it was when the spec was generated, and a SKU can sell out between then
|
|
97
|
-
* and this render. Grid and carousel lose just the products that did; a
|
|
98
|
-
* bundle loses itself entirely if any one of its members did. The rest carry
|
|
99
|
-
* their own words.
|
|
100
|
-
*/
|
|
78
|
+
// Whether a block still has anything to say once the catalog is applied. A SKU
|
|
79
|
+
// can sell out between generating a spec and rendering it — the row goes, or it
|
|
80
|
+
// is flagged out of stock, and both read the same here: grid and carousel lose
|
|
81
|
+
// the products that did, a bundle loses itself if any one member did.
|
|
101
82
|
function hasContent(
|
|
102
83
|
block: Block,
|
|
103
84
|
products: ReadonlyMap<string, Product>,
|
|
@@ -106,7 +87,9 @@ function hasContent(
|
|
|
106
87
|
switch (block.kind) {
|
|
107
88
|
case 'grid':
|
|
108
89
|
case 'carousel':
|
|
109
|
-
return block.items.some(
|
|
90
|
+
return block.items.some(
|
|
91
|
+
(reference) => sellableProduct(products, reference.sku) !== undefined,
|
|
92
|
+
);
|
|
110
93
|
case 'hero':
|
|
111
94
|
case 'banner':
|
|
112
95
|
case 'copy':
|
|
@@ -114,10 +97,12 @@ function hasContent(
|
|
|
114
97
|
case 'bundle': {
|
|
115
98
|
if (block.bundleId === null) return false;
|
|
116
99
|
const bundle = bundles.get(block.bundleId);
|
|
117
|
-
return
|
|
100
|
+
return (
|
|
101
|
+
bundle !== undefined &&
|
|
102
|
+
bundle.skus.every((sku) => sellableProduct(products, sku) !== undefined)
|
|
103
|
+
);
|
|
118
104
|
}
|
|
119
105
|
default:
|
|
120
|
-
// A kind this renderer predates renders nothing, so it counts as nothing.
|
|
121
106
|
block satisfies never;
|
|
122
107
|
return false;
|
|
123
108
|
}
|
|
@@ -143,9 +128,7 @@ function renderBlock(
|
|
|
143
128
|
case 'bundle':
|
|
144
129
|
return <registry.bundle key={index} block={block} context={context} />;
|
|
145
130
|
default:
|
|
146
|
-
// A
|
|
147
|
-
// block rather than the page. In this repo the assertion below fails the
|
|
148
|
-
// build instead, which is the moment it is cheap to notice.
|
|
131
|
+
// A block kind this renderer predates loses that block, not the page.
|
|
149
132
|
block satisfies never;
|
|
150
133
|
return null;
|
|
151
134
|
}
|
|
@@ -185,17 +168,12 @@ export function RudraComponent({
|
|
|
185
168
|
formatBundlePrice: formatBundlePrice ?? ((bundle) => defaultFormatBundlePrice(bundle, locale)),
|
|
186
169
|
};
|
|
187
170
|
|
|
188
|
-
// An empty recommendation area is worse than none: it takes up space and
|
|
189
|
-
// tells the shopper the page is broken. That includes the subtler version —
|
|
190
|
-
// a headline and an empty box, because every product in the spec has sold out
|
|
191
|
-
// since it was generated — which is why this asks what is left rather than
|
|
192
|
-
// how many blocks arrived.
|
|
193
171
|
const visible = spec.blocks.filter((block) =>
|
|
194
172
|
hasContent(block, context.products, context.bundles),
|
|
195
173
|
);
|
|
196
174
|
if (visible.length === 0) return null;
|
|
197
175
|
|
|
198
|
-
// React
|
|
176
|
+
// React drops a data-* attribute whose value is undefined, so degradedReason
|
|
199
177
|
// needs no branch of its own.
|
|
200
178
|
const diagnosticAttributes = hasDiagnostics
|
|
201
179
|
? {
|
|
@@ -208,15 +186,8 @@ export function RudraComponent({
|
|
|
208
186
|
|
|
209
187
|
return (
|
|
210
188
|
<section
|
|
211
|
-
// Extended rather than replaced: every child class is namespaced under
|
|
212
|
-
// `rudra`, and the package ships no stylesheet, so a host will pass one.
|
|
213
189
|
className={className ? `rudra ${className}` : 'rudra'}
|
|
214
190
|
data-rudra-slot={spec.slot}
|
|
215
|
-
// Where the component came from travels with the markup on purpose, so
|
|
216
|
-
// hit rate and fallback share can be read off a rendered page — which
|
|
217
|
-
// means `source="fallback"` is public. What stays behind the diagnostics
|
|
218
|
-
// flag is everything more specific than that: which vendor, which model,
|
|
219
|
-
// how slow, and why it fell back.
|
|
220
191
|
data-rudra-source={spec.source}
|
|
221
192
|
data-rudra-tone={spec.tone}
|
|
222
193
|
{...diagnosticAttributes}
|