@vue-storefront/next 7.0.2 → 8.0.0-next.4

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.
@@ -0,0 +1,32 @@
1
+ //#region \0@oxc-project+runtime@0.115.0/helpers/asyncToGenerator.js
2
+ function asyncGeneratorStep(n, t, e, r, o, a, c) {
3
+ try {
4
+ var i = n[a](c), u = i.value;
5
+ } catch (n) {
6
+ e(n);
7
+ return;
8
+ }
9
+ i.done ? t(u) : Promise.resolve(u).then(r, o);
10
+ }
11
+ function _asyncToGenerator(n) {
12
+ return function() {
13
+ var t = this, e = arguments;
14
+ return new Promise(function(r, o) {
15
+ var a = n.apply(t, e);
16
+ function _next(n) {
17
+ asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
18
+ }
19
+ function _throw(n) {
20
+ asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
21
+ }
22
+ _next(void 0);
23
+ });
24
+ };
25
+ }
26
+ //#endregion
27
+ Object.defineProperty(exports, "_asyncToGenerator", {
28
+ enumerable: true,
29
+ get: function() {
30
+ return _asyncToGenerator;
31
+ }
32
+ });
@@ -0,0 +1,27 @@
1
+ //#region \0@oxc-project+runtime@0.115.0/helpers/asyncToGenerator.js
2
+ function asyncGeneratorStep(n, t, e, r, o, a, c) {
3
+ try {
4
+ var i = n[a](c), u = i.value;
5
+ } catch (n) {
6
+ e(n);
7
+ return;
8
+ }
9
+ i.done ? t(u) : Promise.resolve(u).then(r, o);
10
+ }
11
+ function _asyncToGenerator(n) {
12
+ return function() {
13
+ var t = this, e = arguments;
14
+ return new Promise(function(r, o) {
15
+ var a = n.apply(t, e);
16
+ function _next(n) {
17
+ asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
18
+ }
19
+ function _throw(n) {
20
+ asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
21
+ }
22
+ _next(void 0);
23
+ });
24
+ };
25
+ }
26
+ //#endregion
27
+ export { _asyncToGenerator as t };
package/dist/client.cjs CHANGED
@@ -206,7 +206,79 @@ function createAlokaiContext() {
206
206
  }, rest);
207
207
  }
208
208
  //#endregion
209
+ //#region src/storefront-events.ts
210
+ /**
211
+ * Creates a typed pub/sub for storefront domain events. The app defines its own event map and creates
212
+ * the bound helpers once; modules (analytics, personalization, search) emit and subscribe without the
213
+ * core knowing who listens. Client-only — the subscriber registry is created per call, so emitting from
214
+ * a server component reaches no listeners.
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * 'use client';
219
+ * import { createStorefrontEvents } from '@vue-storefront/next/client';
220
+ *
221
+ * export interface StorefrontEventMap {
222
+ * 'cart:productAdded': { productId: string };
223
+ * }
224
+ *
225
+ * export const { emitStorefrontEvent, subscribeStorefrontEvent, useStorefrontEvent } =
226
+ * createStorefrontEvents<StorefrontEventMap>();
227
+ * ```
228
+ */
229
+ function createStorefrontEvents() {
230
+ const handlers = /* @__PURE__ */ new Map();
231
+ /**
232
+ * Emit a domain event. No-op when nothing is subscribed.
233
+ */
234
+ function emitStorefrontEvent(type, payload) {
235
+ var _handlers$get;
236
+ (_handlers$get = handlers.get(type)) === null || _handlers$get === void 0 || _handlers$get.forEach((handler) => handler(payload));
237
+ }
238
+ /**
239
+ * Subscribe to a domain event. Returns an unsubscribe function.
240
+ */
241
+ function subscribeStorefrontEvent(type, handler) {
242
+ var _handlers$get2;
243
+ const set = (_handlers$get2 = handlers.get(type)) !== null && _handlers$get2 !== void 0 ? _handlers$get2 : /* @__PURE__ */ new Set();
244
+ set.add(handler);
245
+ handlers.set(type, set);
246
+ return () => set.delete(handler);
247
+ }
248
+ /**
249
+ * Subscribe to a domain event for the lifetime of the calling component. The handler is read through
250
+ * a ref so an inline (unmemoized) handler doesn't re-subscribe on every render.
251
+ */
252
+ function useStorefrontEvent(type, handler) {
253
+ const handlerRef = (0, react.useRef)(handler);
254
+ handlerRef.current = handler;
255
+ (0, react.useEffect)(() => subscribeStorefrontEvent(type, (payload) => handlerRef.current(payload)), [type]);
256
+ }
257
+ /**
258
+ * Emits a domain event once on mount. Lets server-rendered pages (which cannot call the client
259
+ * emitter directly) announce page-level events such as a PDP view. Give it a `key` tied to the entity
260
+ * (e.g. the product id + sku) so it remounts — and re-emits — when navigating between same-route
261
+ * pages. A ref guard keeps React Strict Mode's double-invoked effect (dev) to a single emit per mount.
262
+ */
263
+ function StorefrontEventEmitter({ event, payload }) {
264
+ const didEmit = (0, react.useRef)(false);
265
+ (0, react.useEffect)(() => {
266
+ if (didEmit.current) return;
267
+ didEmit.current = true;
268
+ emitStorefrontEvent(event, payload);
269
+ }, []);
270
+ return null;
271
+ }
272
+ return {
273
+ emitStorefrontEvent,
274
+ StorefrontEventEmitter,
275
+ subscribeStorefrontEvent,
276
+ useStorefrontEvent
277
+ };
278
+ }
279
+ //#endregion
209
280
  exports.__toESM = __toESM;
210
281
  exports.createAlokaiContext = createAlokaiContext;
211
282
  exports.createSfStateProvider = createSfStateProvider;
283
+ exports.createStorefrontEvents = createStorefrontEvents;
212
284
  exports.env = require_env.env;
package/dist/client.d.cts CHANGED
@@ -40,4 +40,41 @@ import { SDKApi } from "@alokai/connect/sdk";
40
40
  */
41
41
  declare function createAlokaiContext<TSdk extends SDKApi<any>, TSfContract extends SfContract>(): CreateSdkContextReturn<TSdk, TSfContract>;
42
42
  //#endregion
43
- export { type CreateSdkContextReturn, Maybe, SfContract, SfState, SfStateProps, createAlokaiContext, createSfStateProvider, env };
43
+ //#region src/storefront-events.d.ts
44
+ /**
45
+ * Handler for a storefront event of type `TType` in the event map `TMap`.
46
+ */
47
+ type StorefrontEventHandler<TMap, TType extends keyof TMap> = (payload: TMap[TType]) => void;
48
+ /**
49
+ * Creates a typed pub/sub for storefront domain events. The app defines its own event map and creates
50
+ * the bound helpers once; modules (analytics, personalization, search) emit and subscribe without the
51
+ * core knowing who listens. Client-only — the subscriber registry is created per call, so emitting from
52
+ * a server component reaches no listeners.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * 'use client';
57
+ * import { createStorefrontEvents } from '@vue-storefront/next/client';
58
+ *
59
+ * export interface StorefrontEventMap {
60
+ * 'cart:productAdded': { productId: string };
61
+ * }
62
+ *
63
+ * export const { emitStorefrontEvent, subscribeStorefrontEvent, useStorefrontEvent } =
64
+ * createStorefrontEvents<StorefrontEventMap>();
65
+ * ```
66
+ */
67
+ declare function createStorefrontEvents<TMap>(): {
68
+ emitStorefrontEvent: <TType extends keyof TMap>(type: TType, payload: TMap[TType]) => void;
69
+ StorefrontEventEmitter: <TType extends keyof TMap>({
70
+ event,
71
+ payload
72
+ }: {
73
+ event: TType;
74
+ payload: TMap[TType];
75
+ }) => null;
76
+ subscribeStorefrontEvent: <TType extends keyof TMap>(type: TType, handler: StorefrontEventHandler<TMap, TType>) => () => void;
77
+ useStorefrontEvent: <TType extends keyof TMap>(type: TType, handler: StorefrontEventHandler<TMap, TType>) => void;
78
+ };
79
+ //#endregion
80
+ export { type CreateSdkContextReturn, Maybe, SfContract, SfState, SfStateProps, StorefrontEventHandler, createAlokaiContext, createSfStateProvider, createStorefrontEvents, env };
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { c as SfContract, d as createSfStateProvider, l as SfState, r as CreateSdkContextReturn, s as Maybe, t as env, u as SfStateProps } from "./index-BPhD221E.mjs";
1
+ import { c as SfContract, d as createSfStateProvider, l as SfState, r as CreateSdkContextReturn, s as Maybe, t as env, u as SfStateProps } from "./index-7rf5ab-V.mjs";
2
2
  import { SDKApi } from "@alokai/connect/sdk";
3
3
 
4
4
  //#region src/alokai-provider.d.ts
@@ -40,4 +40,41 @@ import { SDKApi } from "@alokai/connect/sdk";
40
40
  */
41
41
  declare function createAlokaiContext<TSdk extends SDKApi<any>, TSfContract extends SfContract>(): CreateSdkContextReturn<TSdk, TSfContract>;
42
42
  //#endregion
43
- export { type CreateSdkContextReturn, Maybe, SfContract, SfState, SfStateProps, createAlokaiContext, createSfStateProvider, env };
43
+ //#region src/storefront-events.d.ts
44
+ /**
45
+ * Handler for a storefront event of type `TType` in the event map `TMap`.
46
+ */
47
+ type StorefrontEventHandler<TMap, TType extends keyof TMap> = (payload: TMap[TType]) => void;
48
+ /**
49
+ * Creates a typed pub/sub for storefront domain events. The app defines its own event map and creates
50
+ * the bound helpers once; modules (analytics, personalization, search) emit and subscribe without the
51
+ * core knowing who listens. Client-only — the subscriber registry is created per call, so emitting from
52
+ * a server component reaches no listeners.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * 'use client';
57
+ * import { createStorefrontEvents } from '@vue-storefront/next/client';
58
+ *
59
+ * export interface StorefrontEventMap {
60
+ * 'cart:productAdded': { productId: string };
61
+ * }
62
+ *
63
+ * export const { emitStorefrontEvent, subscribeStorefrontEvent, useStorefrontEvent } =
64
+ * createStorefrontEvents<StorefrontEventMap>();
65
+ * ```
66
+ */
67
+ declare function createStorefrontEvents<TMap>(): {
68
+ emitStorefrontEvent: <TType extends keyof TMap>(type: TType, payload: TMap[TType]) => void;
69
+ StorefrontEventEmitter: <TType extends keyof TMap>({
70
+ event,
71
+ payload
72
+ }: {
73
+ event: TType;
74
+ payload: TMap[TType];
75
+ }) => null;
76
+ subscribeStorefrontEvent: <TType extends keyof TMap>(type: TType, handler: StorefrontEventHandler<TMap, TType>) => () => void;
77
+ useStorefrontEvent: <TType extends keyof TMap>(type: TType, handler: StorefrontEventHandler<TMap, TType>) => void;
78
+ };
79
+ //#endregion
80
+ export { type CreateSdkContextReturn, Maybe, SfContract, SfState, SfStateProps, StorefrontEventHandler, createAlokaiContext, createSfStateProvider, createStorefrontEvents, env };
package/dist/client.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { n as _objectSpread2, r as PUBLIC_ENV_KEY, t as env } from "./env-9UiGUnbx.mjs";
3
3
  import { AlokaiInstrumentation } from "@alokai/instrumentation-next-component";
4
4
  import Script from "next/script";
5
- import React, { createContext, useContext, useRef } from "react";
5
+ import React, { createContext, useContext, useEffect, useRef } from "react";
6
6
  import { createStore, useStore } from "zustand";
7
7
  //#region src/env/get-public-env.ts
8
8
  /**
@@ -181,4 +181,75 @@ function createAlokaiContext() {
181
181
  }, rest);
182
182
  }
183
183
  //#endregion
184
- export { createAlokaiContext, createSfStateProvider, env };
184
+ //#region src/storefront-events.ts
185
+ /**
186
+ * Creates a typed pub/sub for storefront domain events. The app defines its own event map and creates
187
+ * the bound helpers once; modules (analytics, personalization, search) emit and subscribe without the
188
+ * core knowing who listens. Client-only — the subscriber registry is created per call, so emitting from
189
+ * a server component reaches no listeners.
190
+ *
191
+ * @example
192
+ * ```ts
193
+ * 'use client';
194
+ * import { createStorefrontEvents } from '@vue-storefront/next/client';
195
+ *
196
+ * export interface StorefrontEventMap {
197
+ * 'cart:productAdded': { productId: string };
198
+ * }
199
+ *
200
+ * export const { emitStorefrontEvent, subscribeStorefrontEvent, useStorefrontEvent } =
201
+ * createStorefrontEvents<StorefrontEventMap>();
202
+ * ```
203
+ */
204
+ function createStorefrontEvents() {
205
+ const handlers = /* @__PURE__ */ new Map();
206
+ /**
207
+ * Emit a domain event. No-op when nothing is subscribed.
208
+ */
209
+ function emitStorefrontEvent(type, payload) {
210
+ var _handlers$get;
211
+ (_handlers$get = handlers.get(type)) === null || _handlers$get === void 0 || _handlers$get.forEach((handler) => handler(payload));
212
+ }
213
+ /**
214
+ * Subscribe to a domain event. Returns an unsubscribe function.
215
+ */
216
+ function subscribeStorefrontEvent(type, handler) {
217
+ var _handlers$get2;
218
+ const set = (_handlers$get2 = handlers.get(type)) !== null && _handlers$get2 !== void 0 ? _handlers$get2 : /* @__PURE__ */ new Set();
219
+ set.add(handler);
220
+ handlers.set(type, set);
221
+ return () => set.delete(handler);
222
+ }
223
+ /**
224
+ * Subscribe to a domain event for the lifetime of the calling component. The handler is read through
225
+ * a ref so an inline (unmemoized) handler doesn't re-subscribe on every render.
226
+ */
227
+ function useStorefrontEvent(type, handler) {
228
+ const handlerRef = useRef(handler);
229
+ handlerRef.current = handler;
230
+ useEffect(() => subscribeStorefrontEvent(type, (payload) => handlerRef.current(payload)), [type]);
231
+ }
232
+ /**
233
+ * Emits a domain event once on mount. Lets server-rendered pages (which cannot call the client
234
+ * emitter directly) announce page-level events such as a PDP view. Give it a `key` tied to the entity
235
+ * (e.g. the product id + sku) so it remounts — and re-emits — when navigating between same-route
236
+ * pages. A ref guard keeps React Strict Mode's double-invoked effect (dev) to a single emit per mount.
237
+ */
238
+ function StorefrontEventEmitter({ event, payload }) {
239
+ const didEmit = useRef(false);
240
+ useEffect(() => {
241
+ if (didEmit.current) return;
242
+ didEmit.current = true;
243
+ emitStorefrontEvent(event, payload);
244
+ }, []);
245
+ return null;
246
+ }
247
+ return {
248
+ emitStorefrontEvent,
249
+ StorefrontEventEmitter,
250
+ subscribeStorefrontEvent,
251
+ useStorefrontEvent
252
+ };
253
+ }
254
+ //#endregion
255
+ export { createAlokaiContext, createSfStateProvider, createStorefrontEvents, env };
package/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_env = require("./env-BK6gqfbp.cjs");
3
3
  require("./client.cjs");
4
+ const require_asyncToGenerator = require("./asyncToGenerator-BI_VpA62.cjs");
4
5
  let _alokai_connect_logger = require("@alokai/connect/logger");
5
6
  let _alokai_connect_sdk = require("@alokai/connect/sdk");
6
7
  //#region src/logger/injectMetadata.ts
@@ -20,6 +21,207 @@ const createLogger = (options) => {
20
21
  return injectMetadata(_alokai_connect_logger.LoggerFactory.create(_alokai_connect_logger.LoggerType.ConsolaGcp, options), { alokai: { context: "storefront" } });
21
22
  };
22
23
  //#endregion
24
+ //#region src/image-optimizer/constants.ts
25
+ /**
26
+ * URL prefix the loader emits and the route folder must use:
27
+ * `app/img-proxy/[host]/[...path]/route.ts`.
28
+ */
29
+ const IMG_PROXY_PREFIX = "/img-proxy";
30
+ /**
31
+ * Cache-Control applied to successful (2xx) upstream responses unless
32
+ * overridden per host via `cacheControl`.
33
+ */
34
+ const DEFAULT_CACHE_CONTROL = "public, max-age=3600, s-maxage=86400";
35
+ /**
36
+ * Cache-Control applied to every non-2xx or failed response so errors are
37
+ * never cached by the browser or the CDN.
38
+ */
39
+ const ERROR_CACHE_CONTROL = "no-store, no-cache, must-revalidate";
40
+ const variants = {
41
+ default: {
42
+ buildUpstreamUrl: (mediaHost, segments) => `${mediaHost}/${joinEncoded(segments)}`,
43
+ encodePath: (pathname) => pathname === "" ? void 0 : pathname
44
+ },
45
+ sapcc: {
46
+ buildUpstreamUrl: (mediaHost, segments) => {
47
+ const [context = "", ...path] = segments;
48
+ return `${mediaHost}/${joinEncoded(path)}?context=${encodeURIComponent(context)}`;
49
+ },
50
+ encodePath: (pathname, searchParams) => {
51
+ var _pathname$split$pop;
52
+ const context = searchParams.get("context");
53
+ if (!context) return;
54
+ const hasExtension = ((_pathname$split$pop = pathname.split("/").pop()) !== null && _pathname$split$pop !== void 0 ? _pathname$split$pop : "").includes(".");
55
+ const separator = pathname.endsWith("/") ? "" : "/";
56
+ const safePathname = hasExtension ? pathname : `${pathname}${separator}image.png`;
57
+ return `/${encodeURIComponent(context)}${safePathname}`;
58
+ }
59
+ }
60
+ };
61
+ function joinEncoded(segments) {
62
+ return segments.map((segment) => encodeURIComponent(segment)).join("/");
63
+ }
64
+ //#endregion
65
+ //#region src/image-optimizer/resolve-hosts.ts
66
+ /**
67
+ * Normalizes the `hosts` config into an ordered list of fully-resolved host
68
+ * configs. Throws on misconfiguration so mistakes surface at module init
69
+ * instead of as silently unoptimized images.
70
+ *
71
+ * @internal
72
+ */
73
+ function resolveHosts(hosts) {
74
+ return Object.entries(hosts).map(([key, config]) => resolveHost(key, config));
75
+ }
76
+ /**
77
+ * Derives the conventional env variable name from a host key, e.g.
78
+ * `ct` -> `NEXT_PUBLIC_CT_MEDIA_HOST`, `my-cms` -> `NEXT_PUBLIC_MY_CMS_MEDIA_HOST`.
79
+ *
80
+ * @internal
81
+ */
82
+ function deriveMediaHostEnvName(hostKey) {
83
+ return `NEXT_PUBLIC_${hostKey.replaceAll("-", "_").toUpperCase()}_MEDIA_HOST`;
84
+ }
85
+ const HOST_KEY_PATTERN = /^(?=.*[a-z])[a-z0-9-]+$/;
86
+ function resolveHost(key, config) {
87
+ var _config$variant, _config$buildUpstream, _config$cacheControl, _config$encodePath;
88
+ if (!HOST_KEY_PATTERN.test(key)) throw new Error(`Invalid image optimizer host key "${key}". Use lowercase letters, digits and hyphens only - the key becomes a URL segment and an env variable name fragment.`);
89
+ const variant = variants[(_config$variant = config.variant) !== null && _config$variant !== void 0 ? _config$variant : "default"];
90
+ return {
91
+ buildUpstreamUrl: (_config$buildUpstream = config.buildUpstreamUrl) !== null && _config$buildUpstream !== void 0 ? _config$buildUpstream : variant.buildUpstreamUrl,
92
+ cacheControl: (_config$cacheControl = config.cacheControl) !== null && _config$cacheControl !== void 0 ? _config$cacheControl : DEFAULT_CACHE_CONTROL,
93
+ encodePath: (_config$encodePath = config.encodePath) !== null && _config$encodePath !== void 0 ? _config$encodePath : variant.encodePath,
94
+ key,
95
+ loader: config.loader,
96
+ mediaHostEnvName: deriveMediaHostEnvName(key)
97
+ };
98
+ }
99
+ //#endregion
100
+ //#region src/image-optimizer/create-image-optimizer.ts
101
+ /**
102
+ * Creates a Next.js custom image `loader` and a proxy route-handler `GET`
103
+ * that together let an image CDN in front of the storefront (the Alokai
104
+ * Image Optimizer)
105
+ * optimize media-host images.
106
+ *
107
+ * The loader rewrites `src` values matching a configured media host to
108
+ * `/img-proxy/{key}/{path}?width=&quality=&format=auto`; the route handler
109
+ * proxies that path back to the media host and streams the body so the CDN
110
+ * can transform it on the way to the browser. Non-matching `src` values are
111
+ * returned unchanged.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * // config/image-optimizer.ts - the single place the config exists; the
116
+ * // default export lets `images.loaderFile` point straight at this file
117
+ * import { createImageOptimizer } from "@vue-storefront/next";
118
+ *
119
+ * export const { GET, loader } = createImageOptimizer({ hosts: { ct: {} } });
120
+ *
121
+ * export default loader;
122
+ *
123
+ * // app/img-proxy/[host]/[...path]/route.ts
124
+ * export { GET } from "@/config/image-optimizer";
125
+ * ```
126
+ */
127
+ function createImageOptimizer(config) {
128
+ if (Object.keys(config.hosts).length === 0) throw new Error([
129
+ "createImageOptimizer requires at least one host in \"hosts\".",
130
+ "Add the media hosts whose images the loader should optimize, e.g.:",
131
+ "",
132
+ " createImageOptimizer({",
133
+ " hosts: {",
134
+ " ct: {},",
135
+ " sapcc: { variant: \"sapcc\" },",
136
+ " },",
137
+ " });",
138
+ "",
139
+ "Each key becomes a URL segment and derives the env variable holding",
140
+ "the media host URL, e.g. \"ct\" reads NEXT_PUBLIC_CT_MEDIA_HOST."
141
+ ].join("\n"));
142
+ const hosts = resolveHosts(config.hosts);
143
+ const warnedMissingHosts = /* @__PURE__ */ new Set();
144
+ function loader({ quality, src, width }) {
145
+ for (const host of hosts) {
146
+ const mediaHost = require_env.env(host.mediaHostEnvName);
147
+ if (!mediaHost) {
148
+ if (!warnedMissingHosts.has(host.key)) {
149
+ warnedMissingHosts.add(host.key);
150
+ getLogger().warning(`${host.mediaHostEnvName} is not defined, skipping image optimization for host "${host.key}".`);
151
+ }
152
+ continue;
153
+ }
154
+ const normalizedHost = mediaHost.replace(/\/$/, "");
155
+ if (!matchesMediaHost(src, normalizedHost)) continue;
156
+ if (host.loader) return host.loader({
157
+ quality,
158
+ src,
159
+ width
160
+ });
161
+ const localPath = src.slice(normalizedHost.length);
162
+ const queryIndex = localPath.indexOf("?");
163
+ const pathname = queryIndex === -1 ? localPath : localPath.slice(0, queryIndex);
164
+ const queryString = queryIndex === -1 ? "" : localPath.slice(queryIndex + 1);
165
+ const proxyPath = host.encodePath(pathname, new URLSearchParams(queryString));
166
+ if (proxyPath === void 0) return src;
167
+ return `${IMG_PROXY_PREFIX}/${host.key}${proxyPath}?width=${width}&quality=${quality || 85}&format=auto`;
168
+ }
169
+ return src;
170
+ }
171
+ function GET(_x, _x2) {
172
+ return _GET.apply(this, arguments);
173
+ }
174
+ function _GET() {
175
+ _GET = require_asyncToGenerator._asyncToGenerator(function* (_request, { params }) {
176
+ var _env, _upstream$headers$get;
177
+ const { host: hostKey, path } = yield params;
178
+ const host = hosts.find((entry) => entry.key === hostKey);
179
+ if (!host) return errorResponse(`Unknown image proxy host "${hostKey}"`, 404);
180
+ if (path.length === 0 || path.some(isUnsafeSegment)) return errorResponse("Invalid image path", 400);
181
+ const mediaHost = (_env = require_env.env(host.mediaHostEnvName)) === null || _env === void 0 ? void 0 : _env.replace(/\/$/, "");
182
+ if (!mediaHost) return errorResponse(`${host.mediaHostEnvName} is not defined`, 500);
183
+ let upstream;
184
+ try {
185
+ upstream = yield fetch(host.buildUpstreamUrl(mediaHost, path), { redirect: "manual" });
186
+ } catch (error) {
187
+ getLogger().error(`Failed to fetch upstream image for host "${hostKey}": ${String(error)}`);
188
+ return errorResponse("Failed to fetch upstream image", 502);
189
+ }
190
+ if (upstream.status >= 300 && upstream.status < 400) return errorResponse("Upstream redirect is not allowed", 502);
191
+ return new Response(upstream.body, {
192
+ headers: {
193
+ "cache-control": upstream.ok ? host.cacheControl : ERROR_CACHE_CONTROL,
194
+ "content-type": (_upstream$headers$get = upstream.headers.get("content-type")) !== null && _upstream$headers$get !== void 0 ? _upstream$headers$get : "application/octet-stream"
195
+ },
196
+ status: upstream.status
197
+ });
198
+ });
199
+ return _GET.apply(this, arguments);
200
+ }
201
+ return {
202
+ GET,
203
+ loader
204
+ };
205
+ }
206
+ let logger;
207
+ function getLogger() {
208
+ var _logger;
209
+ (_logger = logger) !== null && _logger !== void 0 || (logger = createLogger());
210
+ return logger;
211
+ }
212
+ function matchesMediaHost(src, normalizedHost) {
213
+ return src === normalizedHost || src.startsWith(`${normalizedHost}/`) || src.startsWith(`${normalizedHost}?`);
214
+ }
215
+ function isUnsafeSegment(segment) {
216
+ return segment === "" || segment === "." || segment === "..";
217
+ }
218
+ function errorResponse(message, status) {
219
+ return new Response(message, {
220
+ headers: { "cache-control": ERROR_CACHE_CONTROL },
221
+ status
222
+ });
223
+ }
224
+ //#endregion
23
225
  //#region src/middleware.ts
24
226
  /**
25
227
  * Creates an Alokai middleware wrapper that adds pathname information to request headers.
@@ -64,32 +266,6 @@ const defaultMethodsRequestConfig = {
64
266
  } }
65
267
  };
66
268
  //#endregion
67
- //#region \0@oxc-project+runtime@0.115.0/helpers/asyncToGenerator.js
68
- function asyncGeneratorStep(n, t, e, r, o, a, c) {
69
- try {
70
- var i = n[a](c), u = i.value;
71
- } catch (n) {
72
- e(n);
73
- return;
74
- }
75
- i.done ? t(u) : Promise.resolve(u).then(r, o);
76
- }
77
- function _asyncToGenerator(n) {
78
- return function() {
79
- var t = this, e = arguments;
80
- return new Promise(function(r, o) {
81
- var a = n.apply(t, e);
82
- function _next(n) {
83
- asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
84
- }
85
- function _throw(n) {
86
- asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
87
- }
88
- _next(void 0);
89
- });
90
- };
91
- }
92
- //#endregion
93
269
  //#region src/sdk/helpers/resolveDynamicContext.ts
94
270
  const BLACKLISTED_HEADERS = new Set(["host"]);
95
271
  function isAppRouterHeaders(headers) {
@@ -112,7 +288,7 @@ function normalizeRequestHeaders(requestHeaders) {
112
288
  }
113
289
  function resolveDynamicContext(context) {
114
290
  return require_env._objectSpread2(require_env._objectSpread2({}, context), {}, { getRequestHeaders() {
115
- return _asyncToGenerator(function* () {
291
+ return require_asyncToGenerator._asyncToGenerator(function* () {
116
292
  var _context$getRequestHe;
117
293
  const normalizedHeaders = normalizeRequestHeaders(yield (_context$getRequestHe = context.getRequestHeaders) === null || _context$getRequestHe === void 0 ? void 0 : _context$getRequestHe.call(context));
118
294
  const requestHeaders = Object.fromEntries(Object.entries(normalizedHeaders).filter(([key]) => !BLACKLISTED_HEADERS.has(key)));
@@ -237,6 +413,7 @@ function buildModules(modules) {
237
413
  }
238
414
  //#endregion
239
415
  exports.createAlokaiMiddleware = createAlokaiMiddleware;
416
+ exports.createImageOptimizer = createImageOptimizer;
240
417
  exports.createLogger = createLogger;
241
418
  exports.createSdk = createSdk;
242
419
  Object.defineProperty(exports, "defineGetConfigSwitcherHeader", {
package/dist/index.d.cts CHANGED
@@ -1,8 +1,128 @@
1
1
  import { a as CreateSdkReturn, i as CreateSdkOptions, n as Config, o as InjectedContext, t as env } from "./index-w0GzdXwC.cjs";
2
2
  import { buildModule, defineGetConfigSwitcherHeader } from "@alokai/connect/sdk";
3
+ import { ImageLoaderProps } from "next/image";
3
4
  import * as _alokai_connect_logger0 from "@alokai/connect/logger";
4
5
  import { NextRequest, NextResponse } from "next/server";
5
6
 
7
+ //#region src/image-optimizer/types.d.ts
8
+ /**
9
+ * Next.js custom image loader produced by `createImageOptimizer`. Pass it as
10
+ * the default export of the file referenced by `images.loaderFile`.
11
+ */
12
+ type ImageOptimizerLoader = (props: ImageLoaderProps) => string;
13
+ /**
14
+ * Route handler produced by `createImageOptimizer`. Re-export it as `GET`
15
+ * from `app/img-proxy/[host]/[...path]/route.ts`.
16
+ */
17
+ type ImageOptimizerRouteHandler = (request: Request, context: ImageOptimizerRouteContext) => Promise<Response>;
18
+ interface ImageOptimizer {
19
+ GET: ImageOptimizerRouteHandler;
20
+ loader: ImageOptimizerLoader;
21
+ }
22
+ interface CreateImageOptimizerConfig {
23
+ /**
24
+ * Media hosts whose images should be optimized, keyed by host key. An
25
+ * empty object means all defaults. The key becomes the `[host]` URL
26
+ * segment and derives the default env variable name:
27
+ * `NEXT_PUBLIC_{KEY}_MEDIA_HOST`.
28
+ */
29
+ hosts: ImageOptimizerHosts;
30
+ }
31
+ type ImageOptimizerHosts = Record<string, ImageOptimizerHostConfig>;
32
+ type ImageOptimizerHostConfig = ImageOptimizerDelegatedHostConfig | ImageOptimizerProxiedHostConfig;
33
+ /**
34
+ * Host with its own image CDN (e.g. Contentful): matching `src` values are
35
+ * delegated to the given loader and never proxied, so the proxy-side options
36
+ * do not apply.
37
+ */
38
+ interface ImageOptimizerDelegatedHostConfig {
39
+ buildUpstreamUrl?: never;
40
+ cacheControl?: never;
41
+ encodePath?: never;
42
+ /**
43
+ * Rewrites a matching `src` directly to the host's own CDN. Same signature
44
+ * as a Next.js image loader, so existing loader implementations plug in.
45
+ */
46
+ loader: ImageOptimizerLoader;
47
+ variant?: never;
48
+ }
49
+ /**
50
+ * Host proxied through `/img-proxy/{key}/...` so the image CDN in front of
51
+ * the storefront (Alokai Image Optimizer) can transform the response.
52
+ */
53
+ interface ImageOptimizerProxiedHostConfig {
54
+ /** Server-side hook overriding the variant's upstream URL reconstruction. */
55
+ buildUpstreamUrl?: BuildUpstreamUrlHook;
56
+ /**
57
+ * Cache-Control for successful (2xx) upstream responses.
58
+ *
59
+ * @default "public, max-age=3600, s-maxage=86400"
60
+ */
61
+ cacheControl?: string;
62
+ /** Client-side hook overriding the variant's path encoding. */
63
+ encodePath?: EncodePathHook;
64
+ loader?: never;
65
+ /**
66
+ * Built-in URL scheme. `sapcc` encodes the `?context=` query parameter as
67
+ * a path segment and appends a synthetic filename when the path has none.
68
+ * Ignored for a side when the corresponding hook is provided.
69
+ *
70
+ * @default "default"
71
+ */
72
+ variant?: ImageOptimizerVariant;
73
+ }
74
+ type ImageOptimizerVariant = "default" | "sapcc";
75
+ /**
76
+ * Builds the path emitted after `/img-proxy/{key}` on the loader (client)
77
+ * side. Receives the pathname relative to the media host (with leading
78
+ * slash, without query string) and the parsed query string of the original
79
+ * `src`. Return the proxy path (starting with `/`), or `undefined` to skip
80
+ * proxying and return the original `src` unchanged.
81
+ */
82
+ type EncodePathHook = (pathname: string, searchParams: URLSearchParams) => string | undefined;
83
+ /**
84
+ * Rebuilds the upstream URL on the route-handler (server) side. Receives the
85
+ * media host (without trailing slash) and the decoded `[...path]` segments
86
+ * from the route params. Must return an absolute URL.
87
+ */
88
+ type BuildUpstreamUrlHook = (mediaHost: string, segments: string[]) => string;
89
+ interface ImageOptimizerRouteContext {
90
+ params: Promise<ImageOptimizerRouteParams>;
91
+ }
92
+ interface ImageOptimizerRouteParams {
93
+ host: string;
94
+ path: string[];
95
+ }
96
+ //#endregion
97
+ //#region src/image-optimizer/create-image-optimizer.d.ts
98
+ /**
99
+ * Creates a Next.js custom image `loader` and a proxy route-handler `GET`
100
+ * that together let an image CDN in front of the storefront (the Alokai
101
+ * Image Optimizer)
102
+ * optimize media-host images.
103
+ *
104
+ * The loader rewrites `src` values matching a configured media host to
105
+ * `/img-proxy/{key}/{path}?width=&quality=&format=auto`; the route handler
106
+ * proxies that path back to the media host and streams the body so the CDN
107
+ * can transform it on the way to the browser. Non-matching `src` values are
108
+ * returned unchanged.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * // config/image-optimizer.ts - the single place the config exists; the
113
+ * // default export lets `images.loaderFile` point straight at this file
114
+ * import { createImageOptimizer } from "@vue-storefront/next";
115
+ *
116
+ * export const { GET, loader } = createImageOptimizer({ hosts: { ct: {} } });
117
+ *
118
+ * export default loader;
119
+ *
120
+ * // app/img-proxy/[host]/[...path]/route.ts
121
+ * export { GET } from "@/config/image-optimizer";
122
+ * ```
123
+ */
124
+ declare function createImageOptimizer(config: CreateImageOptimizerConfig): ImageOptimizer;
125
+ //#endregion
6
126
  //#region src/logger/types.d.ts
7
127
  type LogVerbosity = "alert" | "critical" | "debug" | "emergency" | "error" | "info" | "notice" | "warning";
8
128
  type LoggerOptions = Partial<{
@@ -176,4 +296,4 @@ type DefineSdkModule = (context: InjectedContext) => ReturnType<typeof buildModu
176
296
  */
177
297
  declare function defineSdkModule<TModuleDefinition extends DefineSdkModule>(moduleDefinition: TModuleDefinition): TModuleDefinition;
178
298
  //#endregion
179
- export { type CreateSdkOptions, createAlokaiMiddleware, createLogger, createSdk, defineGetConfigSwitcherHeader, defineSdkConfig, defineSdkModule, env, resolveSdkOptions };
299
+ export { BuildUpstreamUrlHook, CreateImageOptimizerConfig, type CreateSdkOptions, EncodePathHook, ImageOptimizer, ImageOptimizerDelegatedHostConfig, ImageOptimizerHostConfig, ImageOptimizerHosts, ImageOptimizerLoader, ImageOptimizerProxiedHostConfig, ImageOptimizerRouteContext, ImageOptimizerRouteHandler, ImageOptimizerRouteParams, ImageOptimizerVariant, createAlokaiMiddleware, createImageOptimizer, createLogger, createSdk, defineGetConfigSwitcherHeader, defineSdkConfig, defineSdkModule, env, resolveSdkOptions };
package/dist/index.d.mts CHANGED
@@ -1,8 +1,128 @@
1
- import { a as CreateSdkReturn, i as CreateSdkOptions, n as Config, o as InjectedContext, t as env } from "./index-BPhD221E.mjs";
1
+ import { a as CreateSdkReturn, i as CreateSdkOptions, n as Config, o as InjectedContext, t as env } from "./index-7rf5ab-V.mjs";
2
2
  import * as _alokai_connect_logger0 from "@alokai/connect/logger";
3
3
  import { buildModule, defineGetConfigSwitcherHeader } from "@alokai/connect/sdk";
4
+ import { ImageLoaderProps } from "next/image";
4
5
  import { NextRequest, NextResponse } from "next/server";
5
6
 
7
+ //#region src/image-optimizer/types.d.ts
8
+ /**
9
+ * Next.js custom image loader produced by `createImageOptimizer`. Pass it as
10
+ * the default export of the file referenced by `images.loaderFile`.
11
+ */
12
+ type ImageOptimizerLoader = (props: ImageLoaderProps) => string;
13
+ /**
14
+ * Route handler produced by `createImageOptimizer`. Re-export it as `GET`
15
+ * from `app/img-proxy/[host]/[...path]/route.ts`.
16
+ */
17
+ type ImageOptimizerRouteHandler = (request: Request, context: ImageOptimizerRouteContext) => Promise<Response>;
18
+ interface ImageOptimizer {
19
+ GET: ImageOptimizerRouteHandler;
20
+ loader: ImageOptimizerLoader;
21
+ }
22
+ interface CreateImageOptimizerConfig {
23
+ /**
24
+ * Media hosts whose images should be optimized, keyed by host key. An
25
+ * empty object means all defaults. The key becomes the `[host]` URL
26
+ * segment and derives the default env variable name:
27
+ * `NEXT_PUBLIC_{KEY}_MEDIA_HOST`.
28
+ */
29
+ hosts: ImageOptimizerHosts;
30
+ }
31
+ type ImageOptimizerHosts = Record<string, ImageOptimizerHostConfig>;
32
+ type ImageOptimizerHostConfig = ImageOptimizerDelegatedHostConfig | ImageOptimizerProxiedHostConfig;
33
+ /**
34
+ * Host with its own image CDN (e.g. Contentful): matching `src` values are
35
+ * delegated to the given loader and never proxied, so the proxy-side options
36
+ * do not apply.
37
+ */
38
+ interface ImageOptimizerDelegatedHostConfig {
39
+ buildUpstreamUrl?: never;
40
+ cacheControl?: never;
41
+ encodePath?: never;
42
+ /**
43
+ * Rewrites a matching `src` directly to the host's own CDN. Same signature
44
+ * as a Next.js image loader, so existing loader implementations plug in.
45
+ */
46
+ loader: ImageOptimizerLoader;
47
+ variant?: never;
48
+ }
49
+ /**
50
+ * Host proxied through `/img-proxy/{key}/...` so the image CDN in front of
51
+ * the storefront (Alokai Image Optimizer) can transform the response.
52
+ */
53
+ interface ImageOptimizerProxiedHostConfig {
54
+ /** Server-side hook overriding the variant's upstream URL reconstruction. */
55
+ buildUpstreamUrl?: BuildUpstreamUrlHook;
56
+ /**
57
+ * Cache-Control for successful (2xx) upstream responses.
58
+ *
59
+ * @default "public, max-age=3600, s-maxage=86400"
60
+ */
61
+ cacheControl?: string;
62
+ /** Client-side hook overriding the variant's path encoding. */
63
+ encodePath?: EncodePathHook;
64
+ loader?: never;
65
+ /**
66
+ * Built-in URL scheme. `sapcc` encodes the `?context=` query parameter as
67
+ * a path segment and appends a synthetic filename when the path has none.
68
+ * Ignored for a side when the corresponding hook is provided.
69
+ *
70
+ * @default "default"
71
+ */
72
+ variant?: ImageOptimizerVariant;
73
+ }
74
+ type ImageOptimizerVariant = "default" | "sapcc";
75
+ /**
76
+ * Builds the path emitted after `/img-proxy/{key}` on the loader (client)
77
+ * side. Receives the pathname relative to the media host (with leading
78
+ * slash, without query string) and the parsed query string of the original
79
+ * `src`. Return the proxy path (starting with `/`), or `undefined` to skip
80
+ * proxying and return the original `src` unchanged.
81
+ */
82
+ type EncodePathHook = (pathname: string, searchParams: URLSearchParams) => string | undefined;
83
+ /**
84
+ * Rebuilds the upstream URL on the route-handler (server) side. Receives the
85
+ * media host (without trailing slash) and the decoded `[...path]` segments
86
+ * from the route params. Must return an absolute URL.
87
+ */
88
+ type BuildUpstreamUrlHook = (mediaHost: string, segments: string[]) => string;
89
+ interface ImageOptimizerRouteContext {
90
+ params: Promise<ImageOptimizerRouteParams>;
91
+ }
92
+ interface ImageOptimizerRouteParams {
93
+ host: string;
94
+ path: string[];
95
+ }
96
+ //#endregion
97
+ //#region src/image-optimizer/create-image-optimizer.d.ts
98
+ /**
99
+ * Creates a Next.js custom image `loader` and a proxy route-handler `GET`
100
+ * that together let an image CDN in front of the storefront (the Alokai
101
+ * Image Optimizer)
102
+ * optimize media-host images.
103
+ *
104
+ * The loader rewrites `src` values matching a configured media host to
105
+ * `/img-proxy/{key}/{path}?width=&quality=&format=auto`; the route handler
106
+ * proxies that path back to the media host and streams the body so the CDN
107
+ * can transform it on the way to the browser. Non-matching `src` values are
108
+ * returned unchanged.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * // config/image-optimizer.ts - the single place the config exists; the
113
+ * // default export lets `images.loaderFile` point straight at this file
114
+ * import { createImageOptimizer } from "@vue-storefront/next";
115
+ *
116
+ * export const { GET, loader } = createImageOptimizer({ hosts: { ct: {} } });
117
+ *
118
+ * export default loader;
119
+ *
120
+ * // app/img-proxy/[host]/[...path]/route.ts
121
+ * export { GET } from "@/config/image-optimizer";
122
+ * ```
123
+ */
124
+ declare function createImageOptimizer(config: CreateImageOptimizerConfig): ImageOptimizer;
125
+ //#endregion
6
126
  //#region src/logger/types.d.ts
7
127
  type LogVerbosity = "alert" | "critical" | "debug" | "emergency" | "error" | "info" | "notice" | "warning";
8
128
  type LoggerOptions = Partial<{
@@ -176,4 +296,4 @@ type DefineSdkModule = (context: InjectedContext) => ReturnType<typeof buildModu
176
296
  */
177
297
  declare function defineSdkModule<TModuleDefinition extends DefineSdkModule>(moduleDefinition: TModuleDefinition): TModuleDefinition;
178
298
  //#endregion
179
- export { type CreateSdkOptions, createAlokaiMiddleware, createLogger, createSdk, defineGetConfigSwitcherHeader, defineSdkConfig, defineSdkModule, env, resolveSdkOptions };
299
+ export { BuildUpstreamUrlHook, CreateImageOptimizerConfig, type CreateSdkOptions, EncodePathHook, ImageOptimizer, ImageOptimizerDelegatedHostConfig, ImageOptimizerHostConfig, ImageOptimizerHosts, ImageOptimizerLoader, ImageOptimizerProxiedHostConfig, ImageOptimizerRouteContext, ImageOptimizerRouteHandler, ImageOptimizerRouteParams, ImageOptimizerVariant, createAlokaiMiddleware, createImageOptimizer, createLogger, createSdk, defineGetConfigSwitcherHeader, defineSdkConfig, defineSdkModule, env, resolveSdkOptions };
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { n as _objectSpread2, t as env } from "./env-9UiGUnbx.mjs";
2
+ import { t as _asyncToGenerator } from "./asyncToGenerator-Co5zHbo3.mjs";
2
3
  import { LoggerFactory, LoggerType } from "@alokai/connect/logger";
3
4
  import { buildModule, defineGetConfigSwitcherHeader, initSDK, middlewareModule } from "@alokai/connect/sdk";
4
5
  //#region src/logger/injectMetadata.ts
@@ -18,6 +19,207 @@ const createLogger = (options) => {
18
19
  return injectMetadata(LoggerFactory.create(LoggerType.ConsolaGcp, options), { alokai: { context: "storefront" } });
19
20
  };
20
21
  //#endregion
22
+ //#region src/image-optimizer/constants.ts
23
+ /**
24
+ * URL prefix the loader emits and the route folder must use:
25
+ * `app/img-proxy/[host]/[...path]/route.ts`.
26
+ */
27
+ const IMG_PROXY_PREFIX = "/img-proxy";
28
+ /**
29
+ * Cache-Control applied to successful (2xx) upstream responses unless
30
+ * overridden per host via `cacheControl`.
31
+ */
32
+ const DEFAULT_CACHE_CONTROL = "public, max-age=3600, s-maxage=86400";
33
+ /**
34
+ * Cache-Control applied to every non-2xx or failed response so errors are
35
+ * never cached by the browser or the CDN.
36
+ */
37
+ const ERROR_CACHE_CONTROL = "no-store, no-cache, must-revalidate";
38
+ const variants = {
39
+ default: {
40
+ buildUpstreamUrl: (mediaHost, segments) => `${mediaHost}/${joinEncoded(segments)}`,
41
+ encodePath: (pathname) => pathname === "" ? void 0 : pathname
42
+ },
43
+ sapcc: {
44
+ buildUpstreamUrl: (mediaHost, segments) => {
45
+ const [context = "", ...path] = segments;
46
+ return `${mediaHost}/${joinEncoded(path)}?context=${encodeURIComponent(context)}`;
47
+ },
48
+ encodePath: (pathname, searchParams) => {
49
+ var _pathname$split$pop;
50
+ const context = searchParams.get("context");
51
+ if (!context) return;
52
+ const hasExtension = ((_pathname$split$pop = pathname.split("/").pop()) !== null && _pathname$split$pop !== void 0 ? _pathname$split$pop : "").includes(".");
53
+ const separator = pathname.endsWith("/") ? "" : "/";
54
+ const safePathname = hasExtension ? pathname : `${pathname}${separator}image.png`;
55
+ return `/${encodeURIComponent(context)}${safePathname}`;
56
+ }
57
+ }
58
+ };
59
+ function joinEncoded(segments) {
60
+ return segments.map((segment) => encodeURIComponent(segment)).join("/");
61
+ }
62
+ //#endregion
63
+ //#region src/image-optimizer/resolve-hosts.ts
64
+ /**
65
+ * Normalizes the `hosts` config into an ordered list of fully-resolved host
66
+ * configs. Throws on misconfiguration so mistakes surface at module init
67
+ * instead of as silently unoptimized images.
68
+ *
69
+ * @internal
70
+ */
71
+ function resolveHosts(hosts) {
72
+ return Object.entries(hosts).map(([key, config]) => resolveHost(key, config));
73
+ }
74
+ /**
75
+ * Derives the conventional env variable name from a host key, e.g.
76
+ * `ct` -> `NEXT_PUBLIC_CT_MEDIA_HOST`, `my-cms` -> `NEXT_PUBLIC_MY_CMS_MEDIA_HOST`.
77
+ *
78
+ * @internal
79
+ */
80
+ function deriveMediaHostEnvName(hostKey) {
81
+ return `NEXT_PUBLIC_${hostKey.replaceAll("-", "_").toUpperCase()}_MEDIA_HOST`;
82
+ }
83
+ const HOST_KEY_PATTERN = /^(?=.*[a-z])[a-z0-9-]+$/;
84
+ function resolveHost(key, config) {
85
+ var _config$variant, _config$buildUpstream, _config$cacheControl, _config$encodePath;
86
+ if (!HOST_KEY_PATTERN.test(key)) throw new Error(`Invalid image optimizer host key "${key}". Use lowercase letters, digits and hyphens only - the key becomes a URL segment and an env variable name fragment.`);
87
+ const variant = variants[(_config$variant = config.variant) !== null && _config$variant !== void 0 ? _config$variant : "default"];
88
+ return {
89
+ buildUpstreamUrl: (_config$buildUpstream = config.buildUpstreamUrl) !== null && _config$buildUpstream !== void 0 ? _config$buildUpstream : variant.buildUpstreamUrl,
90
+ cacheControl: (_config$cacheControl = config.cacheControl) !== null && _config$cacheControl !== void 0 ? _config$cacheControl : DEFAULT_CACHE_CONTROL,
91
+ encodePath: (_config$encodePath = config.encodePath) !== null && _config$encodePath !== void 0 ? _config$encodePath : variant.encodePath,
92
+ key,
93
+ loader: config.loader,
94
+ mediaHostEnvName: deriveMediaHostEnvName(key)
95
+ };
96
+ }
97
+ //#endregion
98
+ //#region src/image-optimizer/create-image-optimizer.ts
99
+ /**
100
+ * Creates a Next.js custom image `loader` and a proxy route-handler `GET`
101
+ * that together let an image CDN in front of the storefront (the Alokai
102
+ * Image Optimizer)
103
+ * optimize media-host images.
104
+ *
105
+ * The loader rewrites `src` values matching a configured media host to
106
+ * `/img-proxy/{key}/{path}?width=&quality=&format=auto`; the route handler
107
+ * proxies that path back to the media host and streams the body so the CDN
108
+ * can transform it on the way to the browser. Non-matching `src` values are
109
+ * returned unchanged.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * // config/image-optimizer.ts - the single place the config exists; the
114
+ * // default export lets `images.loaderFile` point straight at this file
115
+ * import { createImageOptimizer } from "@vue-storefront/next";
116
+ *
117
+ * export const { GET, loader } = createImageOptimizer({ hosts: { ct: {} } });
118
+ *
119
+ * export default loader;
120
+ *
121
+ * // app/img-proxy/[host]/[...path]/route.ts
122
+ * export { GET } from "@/config/image-optimizer";
123
+ * ```
124
+ */
125
+ function createImageOptimizer(config) {
126
+ if (Object.keys(config.hosts).length === 0) throw new Error([
127
+ "createImageOptimizer requires at least one host in \"hosts\".",
128
+ "Add the media hosts whose images the loader should optimize, e.g.:",
129
+ "",
130
+ " createImageOptimizer({",
131
+ " hosts: {",
132
+ " ct: {},",
133
+ " sapcc: { variant: \"sapcc\" },",
134
+ " },",
135
+ " });",
136
+ "",
137
+ "Each key becomes a URL segment and derives the env variable holding",
138
+ "the media host URL, e.g. \"ct\" reads NEXT_PUBLIC_CT_MEDIA_HOST."
139
+ ].join("\n"));
140
+ const hosts = resolveHosts(config.hosts);
141
+ const warnedMissingHosts = /* @__PURE__ */ new Set();
142
+ function loader({ quality, src, width }) {
143
+ for (const host of hosts) {
144
+ const mediaHost = env(host.mediaHostEnvName);
145
+ if (!mediaHost) {
146
+ if (!warnedMissingHosts.has(host.key)) {
147
+ warnedMissingHosts.add(host.key);
148
+ getLogger().warning(`${host.mediaHostEnvName} is not defined, skipping image optimization for host "${host.key}".`);
149
+ }
150
+ continue;
151
+ }
152
+ const normalizedHost = mediaHost.replace(/\/$/, "");
153
+ if (!matchesMediaHost(src, normalizedHost)) continue;
154
+ if (host.loader) return host.loader({
155
+ quality,
156
+ src,
157
+ width
158
+ });
159
+ const localPath = src.slice(normalizedHost.length);
160
+ const queryIndex = localPath.indexOf("?");
161
+ const pathname = queryIndex === -1 ? localPath : localPath.slice(0, queryIndex);
162
+ const queryString = queryIndex === -1 ? "" : localPath.slice(queryIndex + 1);
163
+ const proxyPath = host.encodePath(pathname, new URLSearchParams(queryString));
164
+ if (proxyPath === void 0) return src;
165
+ return `${IMG_PROXY_PREFIX}/${host.key}${proxyPath}?width=${width}&quality=${quality || 85}&format=auto`;
166
+ }
167
+ return src;
168
+ }
169
+ function GET(_x, _x2) {
170
+ return _GET.apply(this, arguments);
171
+ }
172
+ function _GET() {
173
+ _GET = _asyncToGenerator(function* (_request, { params }) {
174
+ var _env, _upstream$headers$get;
175
+ const { host: hostKey, path } = yield params;
176
+ const host = hosts.find((entry) => entry.key === hostKey);
177
+ if (!host) return errorResponse(`Unknown image proxy host "${hostKey}"`, 404);
178
+ if (path.length === 0 || path.some(isUnsafeSegment)) return errorResponse("Invalid image path", 400);
179
+ const mediaHost = (_env = env(host.mediaHostEnvName)) === null || _env === void 0 ? void 0 : _env.replace(/\/$/, "");
180
+ if (!mediaHost) return errorResponse(`${host.mediaHostEnvName} is not defined`, 500);
181
+ let upstream;
182
+ try {
183
+ upstream = yield fetch(host.buildUpstreamUrl(mediaHost, path), { redirect: "manual" });
184
+ } catch (error) {
185
+ getLogger().error(`Failed to fetch upstream image for host "${hostKey}": ${String(error)}`);
186
+ return errorResponse("Failed to fetch upstream image", 502);
187
+ }
188
+ if (upstream.status >= 300 && upstream.status < 400) return errorResponse("Upstream redirect is not allowed", 502);
189
+ return new Response(upstream.body, {
190
+ headers: {
191
+ "cache-control": upstream.ok ? host.cacheControl : ERROR_CACHE_CONTROL,
192
+ "content-type": (_upstream$headers$get = upstream.headers.get("content-type")) !== null && _upstream$headers$get !== void 0 ? _upstream$headers$get : "application/octet-stream"
193
+ },
194
+ status: upstream.status
195
+ });
196
+ });
197
+ return _GET.apply(this, arguments);
198
+ }
199
+ return {
200
+ GET,
201
+ loader
202
+ };
203
+ }
204
+ let logger;
205
+ function getLogger() {
206
+ var _logger;
207
+ (_logger = logger) !== null && _logger !== void 0 || (logger = createLogger());
208
+ return logger;
209
+ }
210
+ function matchesMediaHost(src, normalizedHost) {
211
+ return src === normalizedHost || src.startsWith(`${normalizedHost}/`) || src.startsWith(`${normalizedHost}?`);
212
+ }
213
+ function isUnsafeSegment(segment) {
214
+ return segment === "" || segment === "." || segment === "..";
215
+ }
216
+ function errorResponse(message, status) {
217
+ return new Response(message, {
218
+ headers: { "cache-control": ERROR_CACHE_CONTROL },
219
+ status
220
+ });
221
+ }
222
+ //#endregion
21
223
  //#region src/middleware.ts
22
224
  /**
23
225
  * Creates an Alokai middleware wrapper that adds pathname information to request headers.
@@ -62,32 +264,6 @@ const defaultMethodsRequestConfig = {
62
264
  } }
63
265
  };
64
266
  //#endregion
65
- //#region \0@oxc-project+runtime@0.115.0/helpers/asyncToGenerator.js
66
- function asyncGeneratorStep(n, t, e, r, o, a, c) {
67
- try {
68
- var i = n[a](c), u = i.value;
69
- } catch (n) {
70
- e(n);
71
- return;
72
- }
73
- i.done ? t(u) : Promise.resolve(u).then(r, o);
74
- }
75
- function _asyncToGenerator(n) {
76
- return function() {
77
- var t = this, e = arguments;
78
- return new Promise(function(r, o) {
79
- var a = n.apply(t, e);
80
- function _next(n) {
81
- asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
82
- }
83
- function _throw(n) {
84
- asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
85
- }
86
- _next(void 0);
87
- });
88
- };
89
- }
90
- //#endregion
91
267
  //#region src/sdk/helpers/resolveDynamicContext.ts
92
268
  const BLACKLISTED_HEADERS = new Set(["host"]);
93
269
  function isAppRouterHeaders(headers) {
@@ -234,4 +410,4 @@ function buildModules(modules) {
234
410
  return (context) => Object.fromEntries(Object.entries(modules).map(([key, module]) => [key, module(context)]));
235
411
  }
236
412
  //#endregion
237
- export { createAlokaiMiddleware, createLogger, createSdk, defineGetConfigSwitcherHeader, defineSdkConfig, defineSdkModule, env, resolveSdkOptions };
413
+ export { createAlokaiMiddleware, createImageOptimizer, createLogger, createSdk, defineGetConfigSwitcherHeader, defineSdkConfig, defineSdkModule, env, resolveSdkOptions };
@@ -0,0 +1,23 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ require("./client.cjs");
3
+ const require_asyncToGenerator = require("./asyncToGenerator-BI_VpA62.cjs");
4
+ let next_headers = require("next/headers");
5
+ //#region src/current-path.ts
6
+ /**
7
+ * Returns the current request path (`pathname` + `search`) inside a Server Component, as exposed by
8
+ * {@link createAlokaiMiddleware} on the request headers. Reading it through this helper keeps the
9
+ * underlying header names (`x-pathname` / `x-search`) an implementation detail of the framework.
10
+ */
11
+ function getCurrentPath() {
12
+ return _getCurrentPath.apply(this, arguments);
13
+ }
14
+ function _getCurrentPath() {
15
+ _getCurrentPath = require_asyncToGenerator._asyncToGenerator(function* () {
16
+ var _requestHeaders$get, _requestHeaders$get2;
17
+ const requestHeaders = yield (0, next_headers.headers)();
18
+ return `${(_requestHeaders$get = requestHeaders.get("x-pathname")) !== null && _requestHeaders$get !== void 0 ? _requestHeaders$get : ""}${(_requestHeaders$get2 = requestHeaders.get("x-search")) !== null && _requestHeaders$get2 !== void 0 ? _requestHeaders$get2 : ""}`;
19
+ });
20
+ return _getCurrentPath.apply(this, arguments);
21
+ }
22
+ //#endregion
23
+ exports.getCurrentPath = getCurrentPath;
@@ -0,0 +1,9 @@
1
+ //#region src/current-path.d.ts
2
+ /**
3
+ * Returns the current request path (`pathname` + `search`) inside a Server Component, as exposed by
4
+ * {@link createAlokaiMiddleware} on the request headers. Reading it through this helper keeps the
5
+ * underlying header names (`x-pathname` / `x-search`) an implementation detail of the framework.
6
+ */
7
+ declare function getCurrentPath(): Promise<string>;
8
+ //#endregion
9
+ export { getCurrentPath };
@@ -0,0 +1,9 @@
1
+ //#region src/current-path.d.ts
2
+ /**
3
+ * Returns the current request path (`pathname` + `search`) inside a Server Component, as exposed by
4
+ * {@link createAlokaiMiddleware} on the request headers. Reading it through this helper keeps the
5
+ * underlying header names (`x-pathname` / `x-search`) an implementation detail of the framework.
6
+ */
7
+ declare function getCurrentPath(): Promise<string>;
8
+ //#endregion
9
+ export { getCurrentPath };
@@ -0,0 +1,21 @@
1
+ import { t as _asyncToGenerator } from "./asyncToGenerator-Co5zHbo3.mjs";
2
+ import { headers } from "next/headers";
3
+ //#region src/current-path.ts
4
+ /**
5
+ * Returns the current request path (`pathname` + `search`) inside a Server Component, as exposed by
6
+ * {@link createAlokaiMiddleware} on the request headers. Reading it through this helper keeps the
7
+ * underlying header names (`x-pathname` / `x-search`) an implementation detail of the framework.
8
+ */
9
+ function getCurrentPath() {
10
+ return _getCurrentPath.apply(this, arguments);
11
+ }
12
+ function _getCurrentPath() {
13
+ _getCurrentPath = _asyncToGenerator(function* () {
14
+ var _requestHeaders$get, _requestHeaders$get2;
15
+ const requestHeaders = yield headers();
16
+ return `${(_requestHeaders$get = requestHeaders.get("x-pathname")) !== null && _requestHeaders$get !== void 0 ? _requestHeaders$get : ""}${(_requestHeaders$get2 = requestHeaders.get("x-search")) !== null && _requestHeaders$get2 !== void 0 ? _requestHeaders$get2 : ""}`;
17
+ });
18
+ return _getCurrentPath.apply(this, arguments);
19
+ }
20
+ //#endregion
21
+ export { getCurrentPath };
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vuestorefront/enterprise.git"
8
8
  },
9
- "version": "7.0.2",
9
+ "version": "8.0.0-next.4",
10
10
  "exports": {
11
11
  ".": {
12
12
  "types": "./dist/index.d.mts",
@@ -17,6 +17,11 @@
17
17
  "types": "./dist/client.d.mts",
18
18
  "import": "./dist/client.mjs",
19
19
  "require": "./dist/client.cjs"
20
+ },
21
+ "./server": {
22
+ "types": "./dist/server.d.mts",
23
+ "import": "./dist/server.mjs",
24
+ "require": "./dist/server.cjs"
20
25
  }
21
26
  },
22
27
  "files": [
@@ -31,11 +36,11 @@
31
36
  "version": "cp CHANGELOG.md ../../docs/enterprise/content/storefront/6.change-log/next.md"
32
37
  },
33
38
  "dependencies": {
34
- "@alokai/instrumentation-next-component": "1.0.3",
39
+ "@alokai/instrumentation-next-component": "1.0.4-next.0",
35
40
  "zustand": "^4.5.4"
36
41
  },
37
42
  "devDependencies": {
38
- "@alokai/connect": "^2.1.0",
43
+ "@alokai/connect": "2.3.0-next.7",
39
44
  "@shared/typescript-config": "1.0.0",
40
45
  "@types/react": "^19",
41
46
  "@types/react-dom": "^19",
@@ -50,13 +55,12 @@
50
55
  "vitest": "4.0.18"
51
56
  },
52
57
  "peerDependencies": {
53
- "@alokai/connect": "^2.1.0",
58
+ "@alokai/connect": "2.3.0-next.7",
54
59
  "next": "^16.0.0",
55
60
  "react": "^19.0.0"
56
61
  },
57
62
  "engines": {
58
- "npm": ">=8.0.0",
59
- "node": ">=18.0.0"
63
+ "node": "^20.10.0 || >=22.14.0"
60
64
  },
61
65
  "publishConfig": {
62
66
  "access": "public"