@decocms/apps 0.21.3 → 0.22.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.
@@ -1,4 +1,5 @@
1
1
  import type { ImgHTMLAttributes } from "react";
2
+ import { forwardRef } from "react";
2
3
 
3
4
  // -------------------------------------------------------------------------
4
5
  // Known asset prefixes that get stripped to produce a relative src path
@@ -34,7 +35,7 @@ export function getImageCdnDomain(): string {
34
35
  // Fit options & optimization types
35
36
  // -------------------------------------------------------------------------
36
37
 
37
- export type FitOptions = "contain" | "cover";
38
+ export type FitOptions = "contain" | "cover" | "fill";
38
39
 
39
40
  export const FACTORS = [1, 2];
40
41
 
@@ -163,92 +164,77 @@ export interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "s
163
164
  height?: number;
164
165
  /** @description Object-fit */
165
166
  fit?: FitOptions;
166
- /** @description Preload the image (adds fetchPriority="high") */
167
+ /**
168
+ * @description Web Vitals (LCP). Injects a `<link rel="preload">` tag
169
+ * alongside the `<img>`, sets `fetchPriority="high"` and `loading="eager"`.
170
+ * Use once per page for the LCP image.
171
+ */
167
172
  preload?: boolean;
173
+ /** @description Media query for responsive preloading (e.g. "(min-width: 768px)") */
174
+ media?: string;
168
175
  }
169
176
 
170
- export function Image({
171
- src,
172
- width,
173
- height,
174
- fit = "cover",
175
- preload,
176
- loading,
177
- decoding,
178
- srcSet: srcSetProp,
179
- sizes,
180
- ...rest
181
- }: ImageProps) {
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);
187
-
188
- return (
189
- <img
190
- {...rest}
191
- src={src}
192
- srcSet={srcSet}
193
- sizes={srcSet ? (sizes ?? "(max-width: 768px) 100vw, 50vw") : undefined}
194
- width={width}
195
- height={height}
196
- loading={loading ?? (preload ? "eager" : "lazy")}
197
- decoding={decoding ?? "async"}
198
- fetchPriority={preload ? "high" : undefined}
199
- />
200
- );
201
- }
202
-
203
- // -------------------------------------------------------------------------
204
- // Picture (responsive art direction)
205
- // -------------------------------------------------------------------------
206
-
207
- export interface PictureSourceProps {
208
- src: string;
209
- width: number;
210
- height?: number;
211
- media: string;
212
- fit?: FitOptions;
213
- }
214
-
215
- export interface PictureProps extends Omit<ImageProps, "sizes"> {
216
- sources: PictureSourceProps[];
217
- }
177
+ export const Image = forwardRef<HTMLImageElement, ImageProps>(
178
+ function Image(
179
+ {
180
+ src,
181
+ width,
182
+ height,
183
+ fit = "cover",
184
+ preload,
185
+ media,
186
+ loading,
187
+ decoding,
188
+ srcSet: srcSetProp,
189
+ sizes,
190
+ fetchPriority,
191
+ ...rest
192
+ },
193
+ ref,
194
+ ) {
195
+ if (!height && typeof process !== "undefined") {
196
+ console.warn(`Missing height. This image will NOT be optimized: ${src}`);
197
+ }
218
198
 
219
- export function Picture({
220
- sources,
221
- src,
222
- width,
223
- height,
224
- fit = "cover",
225
- preload,
226
- ...rest
227
- }: PictureProps) {
228
- return (
229
- <picture>
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}
199
+ const optimizedSrc = getOptimizedMediaUrl({
200
+ originalSrc: src,
201
+ width,
202
+ height,
203
+ fit,
204
+ });
205
+ const srcSet = srcSetProp ?? getSrcSet(src, width, height, fit);
206
+ const resolvedSizes = srcSet
207
+ ? (sizes ?? "(max-width: 768px) 100vw, 50vw")
208
+ : undefined;
209
+
210
+ return (
211
+ <>
212
+ {preload && (
213
+ <link
214
+ as="image"
215
+ rel="preload"
216
+ href={optimizedSrc}
217
+ imageSrcSet={srcSet}
218
+ imageSizes={resolvedSizes}
219
+ fetchPriority={fetchPriority ?? "high"}
220
+ media={media}
239
221
  />
240
- );
241
- })}
242
- <Image
243
- src={src}
244
- width={width}
245
- height={height}
246
- fit={fit}
247
- preload={preload}
248
- {...rest}
249
- />
250
- </picture>
251
- );
252
- }
222
+ )}
223
+ <img
224
+ {...rest}
225
+ src={optimizedSrc}
226
+ srcSet={srcSet}
227
+ sizes={resolvedSizes}
228
+ width={width}
229
+ height={height}
230
+ loading={loading ?? (preload ? "eager" : "lazy")}
231
+ decoding={decoding ?? "async"}
232
+ fetchPriority={preload ? "high" : fetchPriority}
233
+ ref={ref}
234
+ />
235
+ </>
236
+ );
237
+ },
238
+ );
253
239
 
254
240
  export default Image;
@@ -0,0 +1,114 @@
1
+ import {
2
+ createContext,
3
+ forwardRef,
4
+ useContext,
5
+ useMemo,
6
+ type ComponentPropsWithoutRef,
7
+ type ReactNode,
8
+ } from "react";
9
+ import { getOptimizedMediaUrl, getSrcSet, type FitOptions } from "./Image";
10
+
11
+ // -------------------------------------------------------------------------
12
+ // Preload context — flows from <Picture preload> to child <Source> elements
13
+ // so each source can inject its own <link rel="preload"> with the correct
14
+ // media query for responsive art direction.
15
+ // -------------------------------------------------------------------------
16
+
17
+ interface PreloadContextValue {
18
+ preload: boolean;
19
+ }
20
+
21
+ const PreloadContext = createContext<PreloadContextValue>({ preload: false });
22
+
23
+ // -------------------------------------------------------------------------
24
+ // Source — composable <source> with automatic srcSet optimization and
25
+ // preload link injection when inside a <Picture preload>.
26
+ // -------------------------------------------------------------------------
27
+
28
+ export type SourceProps = Omit<
29
+ ComponentPropsWithoutRef<"source">,
30
+ "width" | "height"
31
+ > & {
32
+ src: string;
33
+ /** @description Improves Web Vitals (CLS|LCP) */
34
+ width: number;
35
+ /** @description Improves Web Vitals (CLS|LCP) */
36
+ height?: number;
37
+ /** @description Improves Web Vitals (LCP). Use high for LCP image. */
38
+ fetchPriority?: "high" | "low" | "auto";
39
+ /** @description Object-fit */
40
+ fit?: FitOptions;
41
+ };
42
+
43
+ export const Source = forwardRef<HTMLSourceElement, SourceProps>(
44
+ function Source(
45
+ { src, width, height, fetchPriority, fit = "cover", ...rest },
46
+ ref,
47
+ ) {
48
+ const { preload } = useContext(PreloadContext);
49
+
50
+ const optimizedSrc = getOptimizedMediaUrl({
51
+ originalSrc: src,
52
+ width,
53
+ height,
54
+ fit,
55
+ });
56
+ const srcSet = rest.srcSet ?? getSrcSet(src, width, height, fit);
57
+
58
+ return (
59
+ <>
60
+ {preload && (
61
+ <link
62
+ as="image"
63
+ rel="preload"
64
+ href={optimizedSrc}
65
+ imageSrcSet={srcSet}
66
+ fetchPriority={fetchPriority ?? "high"}
67
+ media={rest.media}
68
+ />
69
+ )}
70
+ <source
71
+ {...rest}
72
+ srcSet={srcSet ?? optimizedSrc}
73
+ width={width}
74
+ height={height}
75
+ ref={ref}
76
+ />
77
+ </>
78
+ );
79
+ },
80
+ );
81
+
82
+ // -------------------------------------------------------------------------
83
+ // Picture — composable wrapper that provides preload context to children.
84
+ //
85
+ // Usage:
86
+ // <Picture preload={isLcp}>
87
+ // <Source media="(max-width: 767px)" src={mobile} width={320} height={280} />
88
+ // <Source media="(min-width: 768px)" src={desktop} width={1280} height={280} />
89
+ // <Image src={desktop} width={1280} height={280} />
90
+ // </Picture>
91
+ // -------------------------------------------------------------------------
92
+
93
+ export type PictureProps = ComponentPropsWithoutRef<"picture"> & {
94
+ children: ReactNode;
95
+ /**
96
+ * @description When true, child <Source> and <Image> elements inject
97
+ * `<link rel="preload">` tags for their respective media queries.
98
+ */
99
+ preload?: boolean;
100
+ };
101
+
102
+ export const Picture = forwardRef<HTMLPictureElement, PictureProps>(
103
+ function Picture({ children, preload = false, ...props }, ref) {
104
+ const value = useMemo(() => ({ preload }), [preload]);
105
+
106
+ return (
107
+ <PreloadContext.Provider value={value}>
108
+ <picture {...props} ref={ref}>
109
+ {children}
110
+ </picture>
111
+ </PreloadContext.Provider>
112
+ );
113
+ },
114
+ );
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@decocms/apps",
3
- "version": "0.21.3",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
5
  "description": "Deco commerce apps for TanStack Start - Shopify, VTEX, commerce types, analytics utils",
6
6
  "exports": {
7
7
  "./commerce/types": "./commerce/types/commerce.ts",
8
8
  "./commerce/components/JsonLd": "./commerce/components/JsonLd.tsx",
9
9
  "./commerce/components/Image": "./commerce/components/Image.tsx",
10
+ "./commerce/components/Picture": "./commerce/components/Picture.tsx",
10
11
  "./commerce/utils/*": "./commerce/utils/*.ts",
11
12
  "./commerce/sdk/*": "./commerce/sdk/*.ts",
12
13
  "./shopify": "./shopify/index.ts",
package/vtex/client.ts CHANGED
@@ -82,6 +82,8 @@ export interface VtexConfig {
82
82
 
83
83
  let _config: VtexConfig | null = null;
84
84
  let _fetch: typeof fetch = globalThis.fetch;
85
+ let _getCookieHeader: (() => string | undefined) | null = null;
86
+ let _forwardSetCookies: ((cookies: string[]) => void) | null = null;
85
87
 
86
88
  export function configureVtex(config: VtexConfig) {
87
89
  _config = config;
@@ -103,6 +105,37 @@ export function setVtexFetch(fetchFn: typeof fetch) {
103
105
  _fetch = fetchFn;
104
106
  }
105
107
 
108
+ /**
109
+ * Register a provider that returns the Cookie header from the current request.
110
+ * Called automatically by vtexFetchWithCookies when no explicit cookieHeader
111
+ * is present in the request init — so checkout/session/auth actions
112
+ * transparently forward browser cookies to the VTEX API.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * import { getRequestHeader } from "@tanstack/react-start/server";
117
+ * setRequestCookieProvider(() => getRequestHeader("cookie") ?? undefined);
118
+ * ```
119
+ */
120
+ export function setRequestCookieProvider(fn: () => string | undefined) {
121
+ _getCookieHeader = fn;
122
+ }
123
+
124
+ /**
125
+ * Register a callback that forwards VTEX Set-Cookie headers back to the browser.
126
+ * Called automatically by vtexFetchWithCookies after every response that
127
+ * carries Set-Cookie headers.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * import { setResponseHeader } from "@tanstack/react-start/server";
132
+ * setResponseCookieForwarder((cookies) => setResponseHeader("set-cookie", cookies));
133
+ * ```
134
+ */
135
+ export function setResponseCookieForwarder(fn: (cookies: string[]) => void) {
136
+ _forwardSetCookies = fn;
137
+ }
138
+
106
139
  export function getVtexConfig(): VtexConfig {
107
140
  if (!_config)
108
141
  throw new Error("VTEX not configured. Call configureVtex() first.");
@@ -222,6 +255,20 @@ export async function vtexFetchWithCookies<T>(
222
255
  path: string,
223
256
  init?: RequestInit,
224
257
  ): Promise<VtexFetchResult<T>> {
258
+ // Auto-inject request cookies when no explicit cookie header is set
259
+ if (_getCookieHeader) {
260
+ const existingHeaders = init?.headers as Record<string, string> | undefined;
261
+ if (!existingHeaders?.["cookie"]) {
262
+ const cookies = _getCookieHeader();
263
+ if (cookies) {
264
+ init = {
265
+ ...init,
266
+ headers: { ...existingHeaders, cookie: cookies },
267
+ };
268
+ }
269
+ }
270
+ }
271
+
225
272
  const response = await vtexFetchResponse(path, init);
226
273
  const data = (await response.json()) as T;
227
274
  const setCookies: string[] = [];
@@ -234,6 +281,12 @@ export async function vtexFetchWithCookies<T>(
234
281
  }
235
282
  });
236
283
  }
284
+
285
+ // Auto-forward Set-Cookie headers to the browser response
286
+ if (_forwardSetCookies && setCookies.length) {
287
+ _forwardSetCookies(setCookies);
288
+ }
289
+
237
290
  return { data, setCookies };
238
291
  }
239
292