@decocms/apps 0.21.1 → 0.21.3
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 +75 -0
- package/commerce/components/Image.tsx +171 -126
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @decocms/apps
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@decocms/apps)
|
|
4
|
+
[](https://github.com/decocms/apps-start/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
Commerce integrations for [Deco](https://deco.cx) storefronts on **TanStack Start + React 19 + Cloudflare Workers**.
|
|
7
|
+
|
|
8
|
+
Provides VTEX and Shopify loaders, actions, hooks, and shared commerce types based on schema.org. Built on top of [`@decocms/start`](https://www.npmjs.com/package/@decocms/start).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @decocms/apps
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Integrations
|
|
17
|
+
|
|
18
|
+
### VTEX
|
|
19
|
+
|
|
20
|
+
Full VTEX Intelligent Search and Checkout integration.
|
|
21
|
+
|
|
22
|
+
| Import | Purpose |
|
|
23
|
+
|--------|---------|
|
|
24
|
+
| `@decocms/apps/vtex` | Configuration and setup |
|
|
25
|
+
| `@decocms/apps/vtex/client` | VTEX API client with SWR caching |
|
|
26
|
+
| `@decocms/apps/vtex/loaders/*` | Product, cart, search, catalog, session, wishlist |
|
|
27
|
+
| `@decocms/apps/vtex/actions/*` | Checkout, auth, newsletter, profile, wishlist |
|
|
28
|
+
| `@decocms/apps/vtex/hooks` | useCart, useUser, useWishlist, useAutocomplete |
|
|
29
|
+
| `@decocms/apps/vtex/inline-loaders/*` | PDP, PLP, product list, suggestions |
|
|
30
|
+
| `@decocms/apps/vtex/middleware` | Cookie propagation and session handling |
|
|
31
|
+
| `@decocms/apps/vtex/invoke` | Server function wrappers |
|
|
32
|
+
| `@decocms/apps/vtex/utils/*` | Transform, enrichment, segment, cookies |
|
|
33
|
+
|
|
34
|
+
### Shopify
|
|
35
|
+
|
|
36
|
+
Storefront API integration via GraphQL.
|
|
37
|
+
|
|
38
|
+
| Import | Purpose |
|
|
39
|
+
|--------|---------|
|
|
40
|
+
| `@decocms/apps/shopify` | Configuration and setup |
|
|
41
|
+
| `@decocms/apps/shopify/client` | Storefront GraphQL client |
|
|
42
|
+
| `@decocms/apps/shopify/loaders/*` | PDP, PLP, product list, cart, user |
|
|
43
|
+
| `@decocms/apps/shopify/actions/cart/*` | Add, update items, coupons |
|
|
44
|
+
| `@decocms/apps/shopify/actions/user/*` | Sign in, sign up |
|
|
45
|
+
| `@decocms/apps/shopify/utils/*` | Transform, cookies, GraphQL queries |
|
|
46
|
+
|
|
47
|
+
### Shared Commerce
|
|
48
|
+
|
|
49
|
+
Platform-agnostic types and utilities.
|
|
50
|
+
|
|
51
|
+
| Import | Purpose |
|
|
52
|
+
|--------|---------|
|
|
53
|
+
| `@decocms/apps/commerce/types` | schema.org Product, Offer, BreadcrumbList, etc. |
|
|
54
|
+
| `@decocms/apps/commerce/components/Image` | Optimized commerce image component |
|
|
55
|
+
| `@decocms/apps/commerce/components/JsonLd` | Structured data for SEO |
|
|
56
|
+
| `@decocms/apps/commerce/sdk/*` | useOffer, formatPrice, analytics, URL utils |
|
|
57
|
+
| `@decocms/apps/commerce/utils/*` | productToAnalyticsItem, canonical, stateByZip |
|
|
58
|
+
|
|
59
|
+
## Peer Dependencies
|
|
60
|
+
|
|
61
|
+
- `@decocms/start` >= 0.19.0
|
|
62
|
+
- `@tanstack/react-query` >= 5
|
|
63
|
+
- `react` >= 18
|
|
64
|
+
- `react-dom` >= 18
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm run typecheck # tsc --noEmit
|
|
70
|
+
npm run check # typecheck + unused export detection
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT
|
|
@@ -1,144 +1,201 @@
|
|
|
1
|
+
import type { ImgHTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
// -------------------------------------------------------------------------
|
|
4
|
+
// Known asset prefixes that get stripped to produce a relative src path
|
|
5
|
+
// -------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
const DECO_CACHE_URL = "https://assets.decocache.com/";
|
|
8
|
+
const S3_URL = "https://deco-sites-assets.s3.sa-east-1.amazonaws.com/";
|
|
9
|
+
|
|
10
|
+
// -------------------------------------------------------------------------
|
|
11
|
+
// Configurable CDN domain
|
|
12
|
+
// -------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
let imageCdnDomain = "decoims.com";
|
|
15
|
+
|
|
1
16
|
/**
|
|
2
|
-
*
|
|
17
|
+
* Register the image CDN domain used by `getOptimizedMediaUrl`.
|
|
18
|
+
* Call once in your site's setup.ts before any page loads.
|
|
3
19
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```tsx
|
|
10
|
-
* <Image
|
|
11
|
-
* src="https://store.vteximg.com.br/products/123.jpg"
|
|
12
|
-
* width={400}
|
|
13
|
-
* height={400}
|
|
14
|
-
* alt="Product name"
|
|
15
|
-
* cdn="vtex"
|
|
16
|
-
* />
|
|
17
|
-
* ```
|
|
20
|
+
* Available domains:
|
|
21
|
+
* - `decoims.com` (Cloudflare, default — best compression, same edge as Workers)
|
|
22
|
+
* - `deco-assets.edgedeco.com` (Azion IMS)
|
|
23
|
+
* - `deco-assets.decoazn.com` (Azion IMS, legacy)
|
|
18
24
|
*/
|
|
25
|
+
export function registerImageCdnDomain(domain: string) {
|
|
26
|
+
imageCdnDomain = domain.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
27
|
+
}
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
export function getImageCdnDomain(): string {
|
|
30
|
+
return imageCdnDomain;
|
|
31
|
+
}
|
|
21
32
|
|
|
22
|
-
|
|
33
|
+
// -------------------------------------------------------------------------
|
|
34
|
+
// Fit options & optimization types
|
|
35
|
+
// -------------------------------------------------------------------------
|
|
23
36
|
|
|
24
|
-
export
|
|
25
|
-
|
|
37
|
+
export type FitOptions = "contain" | "cover";
|
|
38
|
+
|
|
39
|
+
export const FACTORS = [1, 2];
|
|
40
|
+
|
|
41
|
+
interface OptimizationOptions {
|
|
42
|
+
originalSrc: string;
|
|
26
43
|
width: number;
|
|
27
|
-
height
|
|
28
|
-
|
|
29
|
-
cdn?: ImageCDN;
|
|
30
|
-
/**
|
|
31
|
-
* Responsive sizes descriptor.
|
|
32
|
-
* @default "(max-width: 768px) 100vw, 50vw"
|
|
33
|
-
*/
|
|
34
|
-
sizes?: string;
|
|
35
|
-
/**
|
|
36
|
-
* Multipliers for srcset generation.
|
|
37
|
-
* @default [1, 2]
|
|
38
|
-
*/
|
|
39
|
-
srcSetMultipliers?: number[];
|
|
40
|
-
/** Preload the image (adds fetchPriority="high"). */
|
|
41
|
-
preload?: boolean;
|
|
44
|
+
height?: number;
|
|
45
|
+
fit: FitOptions;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
// -------------------------------------------------------------------------
|
|
45
|
-
//
|
|
49
|
+
// Platform-specific URL optimizers (fallbacks when the CDN can handle
|
|
50
|
+
// the native platform's resize syntax directly)
|
|
46
51
|
// -------------------------------------------------------------------------
|
|
47
52
|
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`-${width}-${height}$2`,
|
|
53
|
-
);
|
|
54
|
-
}
|
|
53
|
+
function optimizeVTEX(originalSrc: string, width: number, height?: number): string {
|
|
54
|
+
const src = new URL(originalSrc);
|
|
55
|
+
const [slash, arquivos, ids, rawId, ...rest] = src.pathname.split("/");
|
|
56
|
+
const [trueId] = rawId.split("-");
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
src.pathname = [
|
|
59
|
+
slash,
|
|
60
|
+
arquivos,
|
|
61
|
+
ids,
|
|
62
|
+
`${trueId}-${width}-${height ?? width}`,
|
|
63
|
+
...rest,
|
|
64
|
+
].join("/");
|
|
65
|
+
|
|
66
|
+
return src.href;
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
69
|
+
function optimizeShopify(originalSrc: string, width: number, height?: number): string {
|
|
70
|
+
const url = new URL(originalSrc);
|
|
71
|
+
url.searchParams.set("width", `${width}`);
|
|
72
|
+
if (height) url.searchParams.set("height", `${height}`);
|
|
73
|
+
url.searchParams.set("crop", "center");
|
|
74
|
+
return url.href;
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
// -------------------------------------------------------------------------
|
|
78
|
+
// Core optimization function
|
|
79
|
+
// Ported from deco-cx/apps website/components/Image.tsx
|
|
80
|
+
// -------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Builds an optimized image URL.
|
|
84
|
+
*
|
|
85
|
+
* For Deco-hosted images (decocache / S3), strips the known prefix and
|
|
86
|
+
* routes through the Deco image CDN for edge resize + format conversion.
|
|
87
|
+
*
|
|
88
|
+
* For platform-specific images (VTEX, Shopify), rewrites the URL using
|
|
89
|
+
* the platform's native resize params — no CDN proxy needed.
|
|
90
|
+
*
|
|
91
|
+
* Data URIs are returned as-is.
|
|
92
|
+
*/
|
|
93
|
+
export function getOptimizedMediaUrl(opts: OptimizationOptions): string {
|
|
94
|
+
const { originalSrc, width, height, fit } = opts;
|
|
95
|
+
|
|
96
|
+
if (originalSrc.startsWith("data:")) {
|
|
97
|
+
return originalSrc;
|
|
76
98
|
}
|
|
77
|
-
return src;
|
|
78
|
-
}
|
|
79
99
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
if (
|
|
101
|
+
/(vteximg\.com\.br|vtexassets\.com|myvtex\.com)\/arquivos\/ids\/\d+/.test(originalSrc)
|
|
102
|
+
) {
|
|
103
|
+
return optimizeVTEX(originalSrc, width, height);
|
|
104
|
+
}
|
|
83
105
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
case "vtex": return vtexImageUrl(src, width, height);
|
|
87
|
-
case "shopify": return shopifyImageUrl(src, width);
|
|
88
|
-
case "deco": return decoImageUrl(src, width, height);
|
|
89
|
-
case "cloudflare": return cloudflareImageUrl(src, width, height);
|
|
90
|
-
default: return src;
|
|
106
|
+
if (originalSrc.startsWith("https://cdn.shopify.com")) {
|
|
107
|
+
return optimizeShopify(originalSrc, width, height);
|
|
91
108
|
}
|
|
109
|
+
|
|
110
|
+
const imageSource = originalSrc
|
|
111
|
+
.replace(DECO_CACHE_URL, "")
|
|
112
|
+
.replace(S3_URL, "")
|
|
113
|
+
.split("?")[0];
|
|
114
|
+
|
|
115
|
+
const params = new URLSearchParams();
|
|
116
|
+
params.set("fit", fit);
|
|
117
|
+
params.set("width", `${width}`);
|
|
118
|
+
if (height) params.set("height", `${height}`);
|
|
119
|
+
|
|
120
|
+
return `https://${imageCdnDomain}/image?${params}&src=${imageSource}`;
|
|
92
121
|
}
|
|
93
122
|
|
|
94
|
-
|
|
95
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Generates a srcset string with responsive multipliers.
|
|
125
|
+
*/
|
|
126
|
+
export function getSrcSet(
|
|
127
|
+
originalSrc: string,
|
|
96
128
|
width: number,
|
|
97
|
-
height
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
): string {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
129
|
+
height?: number,
|
|
130
|
+
fit?: FitOptions,
|
|
131
|
+
factors: number[] = FACTORS,
|
|
132
|
+
): string | undefined {
|
|
133
|
+
const entries: string[] = [];
|
|
134
|
+
|
|
135
|
+
for (const factor of factors) {
|
|
136
|
+
const w = Math.trunc(factor * width);
|
|
137
|
+
const h = height ? Math.trunc(factor * height) : undefined;
|
|
138
|
+
|
|
139
|
+
const src = getOptimizedMediaUrl({
|
|
140
|
+
originalSrc,
|
|
141
|
+
width: w,
|
|
142
|
+
height: h,
|
|
143
|
+
fit: fit ?? "cover",
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (src) {
|
|
147
|
+
entries.push(`${src} ${w}w`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return entries.length > 0 ? entries.join(", ") : undefined;
|
|
108
152
|
}
|
|
109
153
|
|
|
110
154
|
// -------------------------------------------------------------------------
|
|
111
|
-
//
|
|
155
|
+
// Image component
|
|
112
156
|
// -------------------------------------------------------------------------
|
|
113
157
|
|
|
158
|
+
export interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "width" | "height"> {
|
|
159
|
+
src: string;
|
|
160
|
+
/** @description Improves Web Vitals (CLS/LCP) */
|
|
161
|
+
width: number;
|
|
162
|
+
/** @description Improves Web Vitals (CLS/LCP) */
|
|
163
|
+
height?: number;
|
|
164
|
+
/** @description Object-fit */
|
|
165
|
+
fit?: FitOptions;
|
|
166
|
+
/** @description Preload the image (adds fetchPriority="high") */
|
|
167
|
+
preload?: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
114
170
|
export function Image({
|
|
115
171
|
src,
|
|
116
172
|
width,
|
|
117
173
|
height,
|
|
118
|
-
|
|
119
|
-
sizes = "(max-width: 768px) 100vw, 50vw",
|
|
120
|
-
srcSetMultipliers = [1, 2],
|
|
174
|
+
fit = "cover",
|
|
121
175
|
preload,
|
|
122
176
|
loading,
|
|
123
177
|
decoding,
|
|
178
|
+
srcSet: srcSetProp,
|
|
179
|
+
sizes,
|
|
124
180
|
...rest
|
|
125
181
|
}: ImageProps) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
182
|
+
if (!height && typeof process !== "undefined") {
|
|
183
|
+
console.warn(`Missing height. This image will NOT be optimized: ${src}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const srcSet = srcSetProp ?? getSrcSet(src, width, height, fit);
|
|
130
187
|
|
|
131
188
|
return (
|
|
132
189
|
<img
|
|
133
|
-
|
|
190
|
+
{...rest}
|
|
191
|
+
src={src}
|
|
134
192
|
srcSet={srcSet}
|
|
135
|
-
sizes={srcSet ? sizes : undefined}
|
|
193
|
+
sizes={srcSet ? (sizes ?? "(max-width: 768px) 100vw, 50vw") : undefined}
|
|
136
194
|
width={width}
|
|
137
195
|
height={height}
|
|
138
196
|
loading={loading ?? (preload ? "eager" : "lazy")}
|
|
139
197
|
decoding={decoding ?? "async"}
|
|
140
198
|
fetchPriority={preload ? "high" : undefined}
|
|
141
|
-
{...rest}
|
|
142
199
|
/>
|
|
143
200
|
);
|
|
144
201
|
}
|
|
@@ -147,63 +204,51 @@ export function Image({
|
|
|
147
204
|
// Picture (responsive art direction)
|
|
148
205
|
// -------------------------------------------------------------------------
|
|
149
206
|
|
|
150
|
-
export interface
|
|
207
|
+
export interface PictureSourceProps {
|
|
151
208
|
src: string;
|
|
152
209
|
width: number;
|
|
153
|
-
height
|
|
210
|
+
height?: number;
|
|
154
211
|
media: string;
|
|
155
|
-
|
|
212
|
+
fit?: FitOptions;
|
|
156
213
|
}
|
|
157
214
|
|
|
158
215
|
export interface PictureProps extends Omit<ImageProps, "sizes"> {
|
|
159
|
-
sources:
|
|
216
|
+
sources: PictureSourceProps[];
|
|
160
217
|
}
|
|
161
218
|
|
|
162
|
-
/**
|
|
163
|
-
* Picture component for responsive art direction.
|
|
164
|
-
*
|
|
165
|
-
* @example
|
|
166
|
-
* ```tsx
|
|
167
|
-
* <Picture
|
|
168
|
-
* sources={[
|
|
169
|
-
* { src: mobileSrc, width: 375, height: 200, media: "(max-width: 767px)", cdn: "vtex" },
|
|
170
|
-
* { src: desktopSrc, width: 1200, height: 400, media: "(min-width: 768px)", cdn: "vtex" },
|
|
171
|
-
* ]}
|
|
172
|
-
* src={desktopSrc}
|
|
173
|
-
* width={1200}
|
|
174
|
-
* height={400}
|
|
175
|
-
* alt="Banner"
|
|
176
|
-
* />
|
|
177
|
-
* ```
|
|
178
|
-
*/
|
|
179
219
|
export function Picture({
|
|
180
220
|
sources,
|
|
181
221
|
src,
|
|
182
222
|
width,
|
|
183
223
|
height,
|
|
184
|
-
|
|
224
|
+
fit = "cover",
|
|
185
225
|
preload,
|
|
186
226
|
...rest
|
|
187
227
|
}: PictureProps) {
|
|
188
228
|
return (
|
|
189
229
|
<picture>
|
|
190
|
-
{sources.map((source, i) =>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
230
|
+
{sources.map((source, i) => {
|
|
231
|
+
const srcSet = getSrcSet(source.src, source.width, source.height, source.fit ?? fit);
|
|
232
|
+
return (
|
|
233
|
+
<source
|
|
234
|
+
key={i}
|
|
235
|
+
srcSet={srcSet}
|
|
236
|
+
media={source.media}
|
|
237
|
+
width={source.width}
|
|
238
|
+
height={source.height}
|
|
239
|
+
/>
|
|
240
|
+
);
|
|
241
|
+
})}
|
|
199
242
|
<Image
|
|
200
243
|
src={src}
|
|
201
244
|
width={width}
|
|
202
245
|
height={height}
|
|
203
|
-
|
|
246
|
+
fit={fit}
|
|
204
247
|
preload={preload}
|
|
205
248
|
{...rest}
|
|
206
249
|
/>
|
|
207
250
|
</picture>
|
|
208
251
|
);
|
|
209
252
|
}
|
|
253
|
+
|
|
254
|
+
export default Image;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/apps",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco commerce apps for TanStack Start - Shopify, VTEX, commerce types, analytics utils",
|
|
6
6
|
"exports": {
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"access": "public"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@decocms/start": "
|
|
64
|
+
"@decocms/start": ">=0.19.0",
|
|
65
65
|
"@tanstack/react-query": ">=5",
|
|
66
66
|
"react": ">=18",
|
|
67
67
|
"react-dom": ">=18"
|