@vue-storefront/next 7.0.2 → 8.0.0-next.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/dist/index.cjs CHANGED
@@ -20,6 +20,231 @@ const createLogger = (options) => {
20
20
  return injectMetadata(_alokai_connect_logger.LoggerFactory.create(_alokai_connect_logger.LoggerType.ConsolaGcp, options), { alokai: { context: "storefront" } });
21
21
  };
22
22
  //#endregion
23
+ //#region src/image-optimizer/constants.ts
24
+ /**
25
+ * URL prefix the loader emits and the route folder must use:
26
+ * `app/img-proxy/[host]/[...path]/route.ts`.
27
+ */
28
+ const IMG_PROXY_PREFIX = "/img-proxy";
29
+ /**
30
+ * Cache-Control applied to successful (2xx) upstream responses unless
31
+ * overridden per host via `cacheControl`.
32
+ */
33
+ const DEFAULT_CACHE_CONTROL = "public, max-age=3600, s-maxage=86400";
34
+ /**
35
+ * Cache-Control applied to every non-2xx or failed response so errors are
36
+ * never cached by the browser or the CDN.
37
+ */
38
+ const ERROR_CACHE_CONTROL = "no-store, no-cache, must-revalidate";
39
+ const variants = {
40
+ default: {
41
+ buildUpstreamUrl: (mediaHost, segments) => `${mediaHost}/${joinEncoded(segments)}`,
42
+ encodePath: (pathname) => pathname === "" ? void 0 : pathname
43
+ },
44
+ sapcc: {
45
+ buildUpstreamUrl: (mediaHost, segments) => {
46
+ const [context = "", ...path] = segments;
47
+ return `${mediaHost}/${joinEncoded(path)}?context=${encodeURIComponent(context)}`;
48
+ },
49
+ encodePath: (pathname, searchParams) => {
50
+ var _pathname$split$pop;
51
+ const context = searchParams.get("context");
52
+ if (!context) return;
53
+ const hasExtension = ((_pathname$split$pop = pathname.split("/").pop()) !== null && _pathname$split$pop !== void 0 ? _pathname$split$pop : "").includes(".");
54
+ const separator = pathname.endsWith("/") ? "" : "/";
55
+ const safePathname = hasExtension ? pathname : `${pathname}${separator}image.png`;
56
+ return `/${encodeURIComponent(context)}${safePathname}`;
57
+ }
58
+ }
59
+ };
60
+ function joinEncoded(segments) {
61
+ return segments.map((segment) => encodeURIComponent(segment)).join("/");
62
+ }
63
+ //#endregion
64
+ //#region src/image-optimizer/resolve-hosts.ts
65
+ /**
66
+ * Normalizes the `hosts` config into an ordered list of fully-resolved host
67
+ * configs. Throws on misconfiguration so mistakes surface at module init
68
+ * instead of as silently unoptimized images.
69
+ *
70
+ * @internal
71
+ */
72
+ function resolveHosts(hosts) {
73
+ return Object.entries(hosts).map(([key, config]) => resolveHost(key, config));
74
+ }
75
+ /**
76
+ * Derives the conventional env variable name from a host key, e.g.
77
+ * `ct` -> `NEXT_PUBLIC_CT_MEDIA_HOST`, `my-cms` -> `NEXT_PUBLIC_MY_CMS_MEDIA_HOST`.
78
+ *
79
+ * @internal
80
+ */
81
+ function deriveMediaHostEnvName(hostKey) {
82
+ return `NEXT_PUBLIC_${hostKey.replaceAll("-", "_").toUpperCase()}_MEDIA_HOST`;
83
+ }
84
+ const HOST_KEY_PATTERN = /^(?=.*[a-z])[a-z0-9-]+$/;
85
+ const ENV_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
86
+ function resolveHost(key, config) {
87
+ var _config$mediaHostEnvN, _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 mediaHostEnvName = (_config$mediaHostEnvN = config.mediaHostEnvName) !== null && _config$mediaHostEnvN !== void 0 ? _config$mediaHostEnvN : deriveMediaHostEnvName(key);
90
+ if (!ENV_NAME_PATTERN.test(mediaHostEnvName)) throw new Error(`Invalid mediaHostEnvName "${mediaHostEnvName}" for host "${key}". Pass the NAME of the env variable (e.g. "NEXT_PUBLIC_CT_MEDIA_HOST"), not its value.`);
91
+ const variant = variants[(_config$variant = config.variant) !== null && _config$variant !== void 0 ? _config$variant : "default"];
92
+ return {
93
+ buildUpstreamUrl: (_config$buildUpstream = config.buildUpstreamUrl) !== null && _config$buildUpstream !== void 0 ? _config$buildUpstream : variant.buildUpstreamUrl,
94
+ cacheControl: (_config$cacheControl = config.cacheControl) !== null && _config$cacheControl !== void 0 ? _config$cacheControl : DEFAULT_CACHE_CONTROL,
95
+ encodePath: (_config$encodePath = config.encodePath) !== null && _config$encodePath !== void 0 ? _config$encodePath : variant.encodePath,
96
+ key,
97
+ loader: config.loader,
98
+ mediaHostEnvName
99
+ };
100
+ }
101
+ //#endregion
102
+ //#region \0@oxc-project+runtime@0.115.0/helpers/asyncToGenerator.js
103
+ function asyncGeneratorStep(n, t, e, r, o, a, c) {
104
+ try {
105
+ var i = n[a](c), u = i.value;
106
+ } catch (n) {
107
+ e(n);
108
+ return;
109
+ }
110
+ i.done ? t(u) : Promise.resolve(u).then(r, o);
111
+ }
112
+ function _asyncToGenerator(n) {
113
+ return function() {
114
+ var t = this, e = arguments;
115
+ return new Promise(function(r, o) {
116
+ var a = n.apply(t, e);
117
+ function _next(n) {
118
+ asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
119
+ }
120
+ function _throw(n) {
121
+ asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
122
+ }
123
+ _next(void 0);
124
+ });
125
+ };
126
+ }
127
+ //#endregion
128
+ //#region src/image-optimizer/create-image-optimizer.ts
129
+ /**
130
+ * Creates a Next.js custom image `loader` and a proxy route-handler `GET`
131
+ * that together let an image CDN in front of the storefront (the Alokai
132
+ * Image Optimizer)
133
+ * optimize media-host images.
134
+ *
135
+ * The loader rewrites `src` values matching a configured media host to
136
+ * `/img-proxy/{key}/{path}?width=&quality=&format=auto`; the route handler
137
+ * proxies that path back to the media host and streams the body so the CDN
138
+ * can transform it on the way to the browser. Non-matching `src` values are
139
+ * returned unchanged.
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * // config/image-optimizer.ts - the single place the config exists; the
144
+ * // default export lets `images.loaderFile` point straight at this file
145
+ * import { createImageOptimizer } from "@vue-storefront/next";
146
+ *
147
+ * export const { GET, loader } = createImageOptimizer({ hosts: { ct: {} } });
148
+ *
149
+ * export default loader;
150
+ *
151
+ * // app/img-proxy/[host]/[...path]/route.ts
152
+ * export { GET } from "@/config/image-optimizer";
153
+ * ```
154
+ */
155
+ function createImageOptimizer(config) {
156
+ if (Object.keys(config.hosts).length === 0) throw new Error([
157
+ "createImageOptimizer requires at least one host in \"hosts\".",
158
+ "Add the media hosts whose images the loader should optimize, e.g.:",
159
+ "",
160
+ " createImageOptimizer({",
161
+ " hosts: {",
162
+ " ct: {},",
163
+ " sapcc: { variant: \"sapcc\" },",
164
+ " },",
165
+ " });",
166
+ "",
167
+ "Each key becomes a URL segment and derives the env variable holding",
168
+ "the media host URL, e.g. \"ct\" reads NEXT_PUBLIC_CT_MEDIA_HOST."
169
+ ].join("\n"));
170
+ const hosts = resolveHosts(config.hosts);
171
+ function loader({ quality, src, width }) {
172
+ for (const host of hosts) {
173
+ const mediaHost = require_env.env(host.mediaHostEnvName);
174
+ if (!mediaHost) {
175
+ getLogger().warning(`${host.mediaHostEnvName} is not defined, skipping image optimization for host "${host.key}".`);
176
+ continue;
177
+ }
178
+ const normalizedHost = mediaHost.replace(/\/$/, "");
179
+ if (!matchesMediaHost(src, normalizedHost)) continue;
180
+ if (host.loader) return host.loader({
181
+ quality,
182
+ src,
183
+ width
184
+ });
185
+ const localPath = src.slice(normalizedHost.length);
186
+ const queryIndex = localPath.indexOf("?");
187
+ const pathname = queryIndex === -1 ? localPath : localPath.slice(0, queryIndex);
188
+ const queryString = queryIndex === -1 ? "" : localPath.slice(queryIndex + 1);
189
+ const proxyPath = host.encodePath(pathname, new URLSearchParams(queryString));
190
+ if (proxyPath === void 0) return src;
191
+ return `${IMG_PROXY_PREFIX}/${host.key}${proxyPath}?width=${width}&quality=${quality || 85}&format=auto`;
192
+ }
193
+ return src;
194
+ }
195
+ function GET(_x, _x2) {
196
+ return _GET.apply(this, arguments);
197
+ }
198
+ function _GET() {
199
+ _GET = _asyncToGenerator(function* (_request, { params }) {
200
+ var _env, _upstream$headers$get;
201
+ const { host: hostKey, path } = yield params;
202
+ const host = hosts.find((entry) => entry.key === hostKey);
203
+ if (!host) return errorResponse(`Unknown image proxy host "${hostKey}"`, 404);
204
+ if (path.length === 0 || path.some(isUnsafeSegment)) return errorResponse("Invalid image path", 400);
205
+ const mediaHost = (_env = require_env.env(host.mediaHostEnvName)) === null || _env === void 0 ? void 0 : _env.replace(/\/$/, "");
206
+ if (!mediaHost) return errorResponse(`${host.mediaHostEnvName} is not defined`, 500);
207
+ let upstream;
208
+ try {
209
+ upstream = yield fetch(host.buildUpstreamUrl(mediaHost, path));
210
+ } catch (error) {
211
+ getLogger().error(`Failed to fetch upstream image for host "${hostKey}": ${String(error)}`);
212
+ return errorResponse("Failed to fetch upstream image", 502);
213
+ }
214
+ return new Response(upstream.body, {
215
+ headers: {
216
+ "cache-control": upstream.ok ? host.cacheControl : ERROR_CACHE_CONTROL,
217
+ "content-type": (_upstream$headers$get = upstream.headers.get("content-type")) !== null && _upstream$headers$get !== void 0 ? _upstream$headers$get : "application/octet-stream"
218
+ },
219
+ status: upstream.status
220
+ });
221
+ });
222
+ return _GET.apply(this, arguments);
223
+ }
224
+ return {
225
+ GET,
226
+ loader
227
+ };
228
+ }
229
+ let logger;
230
+ function getLogger() {
231
+ var _logger;
232
+ (_logger = logger) !== null && _logger !== void 0 || (logger = createLogger());
233
+ return logger;
234
+ }
235
+ function matchesMediaHost(src, normalizedHost) {
236
+ return src === normalizedHost || src.startsWith(`${normalizedHost}/`) || src.startsWith(`${normalizedHost}?`);
237
+ }
238
+ function isUnsafeSegment(segment) {
239
+ return segment === "" || segment === "." || segment === "..";
240
+ }
241
+ function errorResponse(message, status) {
242
+ return new Response(message, {
243
+ headers: { "cache-control": ERROR_CACHE_CONTROL },
244
+ status
245
+ });
246
+ }
247
+ //#endregion
23
248
  //#region src/middleware.ts
24
249
  /**
25
250
  * Creates an Alokai middleware wrapper that adds pathname information to request headers.
@@ -64,32 +289,6 @@ const defaultMethodsRequestConfig = {
64
289
  } }
65
290
  };
66
291
  //#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
292
  //#region src/sdk/helpers/resolveDynamicContext.ts
94
293
  const BLACKLISTED_HEADERS = new Set(["host"]);
95
294
  function isAppRouterHeaders(headers) {
@@ -237,6 +436,7 @@ function buildModules(modules) {
237
436
  }
238
437
  //#endregion
239
438
  exports.createAlokaiMiddleware = createAlokaiMiddleware;
439
+ exports.createImageOptimizer = createImageOptimizer;
240
440
  exports.createLogger = createLogger;
241
441
  exports.createSdk = createSdk;
242
442
  Object.defineProperty(exports, "defineGetConfigSwitcherHeader", {
package/dist/index.d.cts CHANGED
@@ -1,8 +1,137 @@
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 extends ImageOptimizerHostBaseConfig {
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 extends ImageOptimizerHostBaseConfig {
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
+ interface ImageOptimizerHostBaseConfig {
75
+ /**
76
+ * NAME of the env variable holding the media host URL (not the URL
77
+ * itself).
78
+ *
79
+ * @default `NEXT_PUBLIC_{KEY}_MEDIA_HOST` derived from the host key
80
+ */
81
+ mediaHostEnvName?: string;
82
+ }
83
+ type ImageOptimizerVariant = "default" | "sapcc";
84
+ /**
85
+ * Builds the path emitted after `/img-proxy/{key}` on the loader (client)
86
+ * side. Receives the pathname relative to the media host (with leading
87
+ * slash, without query string) and the parsed query string of the original
88
+ * `src`. Return the proxy path (starting with `/`), or `undefined` to skip
89
+ * proxying and return the original `src` unchanged.
90
+ */
91
+ type EncodePathHook = (pathname: string, searchParams: URLSearchParams) => string | undefined;
92
+ /**
93
+ * Rebuilds the upstream URL on the route-handler (server) side. Receives the
94
+ * media host (without trailing slash) and the decoded `[...path]` segments
95
+ * from the route params. Must return an absolute URL.
96
+ */
97
+ type BuildUpstreamUrlHook = (mediaHost: string, segments: string[]) => string;
98
+ interface ImageOptimizerRouteContext {
99
+ params: Promise<ImageOptimizerRouteParams>;
100
+ }
101
+ interface ImageOptimizerRouteParams {
102
+ host: string;
103
+ path: string[];
104
+ }
105
+ //#endregion
106
+ //#region src/image-optimizer/create-image-optimizer.d.ts
107
+ /**
108
+ * Creates a Next.js custom image `loader` and a proxy route-handler `GET`
109
+ * that together let an image CDN in front of the storefront (the Alokai
110
+ * Image Optimizer)
111
+ * optimize media-host images.
112
+ *
113
+ * The loader rewrites `src` values matching a configured media host to
114
+ * `/img-proxy/{key}/{path}?width=&quality=&format=auto`; the route handler
115
+ * proxies that path back to the media host and streams the body so the CDN
116
+ * can transform it on the way to the browser. Non-matching `src` values are
117
+ * returned unchanged.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * // config/image-optimizer.ts - the single place the config exists; the
122
+ * // default export lets `images.loaderFile` point straight at this file
123
+ * import { createImageOptimizer } from "@vue-storefront/next";
124
+ *
125
+ * export const { GET, loader } = createImageOptimizer({ hosts: { ct: {} } });
126
+ *
127
+ * export default loader;
128
+ *
129
+ * // app/img-proxy/[host]/[...path]/route.ts
130
+ * export { GET } from "@/config/image-optimizer";
131
+ * ```
132
+ */
133
+ declare function createImageOptimizer(config: CreateImageOptimizerConfig): ImageOptimizer;
134
+ //#endregion
6
135
  //#region src/logger/types.d.ts
7
136
  type LogVerbosity = "alert" | "critical" | "debug" | "emergency" | "error" | "info" | "notice" | "warning";
8
137
  type LoggerOptions = Partial<{
@@ -176,4 +305,4 @@ type DefineSdkModule = (context: InjectedContext) => ReturnType<typeof buildModu
176
305
  */
177
306
  declare function defineSdkModule<TModuleDefinition extends DefineSdkModule>(moduleDefinition: TModuleDefinition): TModuleDefinition;
178
307
  //#endregion
179
- export { type CreateSdkOptions, createAlokaiMiddleware, createLogger, createSdk, defineGetConfigSwitcherHeader, defineSdkConfig, defineSdkModule, env, resolveSdkOptions };
308
+ 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,137 @@
1
1
  import { a as CreateSdkReturn, i as CreateSdkOptions, n as Config, o as InjectedContext, t as env } from "./index-BPhD221E.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 extends ImageOptimizerHostBaseConfig {
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 extends ImageOptimizerHostBaseConfig {
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
+ interface ImageOptimizerHostBaseConfig {
75
+ /**
76
+ * NAME of the env variable holding the media host URL (not the URL
77
+ * itself).
78
+ *
79
+ * @default `NEXT_PUBLIC_{KEY}_MEDIA_HOST` derived from the host key
80
+ */
81
+ mediaHostEnvName?: string;
82
+ }
83
+ type ImageOptimizerVariant = "default" | "sapcc";
84
+ /**
85
+ * Builds the path emitted after `/img-proxy/{key}` on the loader (client)
86
+ * side. Receives the pathname relative to the media host (with leading
87
+ * slash, without query string) and the parsed query string of the original
88
+ * `src`. Return the proxy path (starting with `/`), or `undefined` to skip
89
+ * proxying and return the original `src` unchanged.
90
+ */
91
+ type EncodePathHook = (pathname: string, searchParams: URLSearchParams) => string | undefined;
92
+ /**
93
+ * Rebuilds the upstream URL on the route-handler (server) side. Receives the
94
+ * media host (without trailing slash) and the decoded `[...path]` segments
95
+ * from the route params. Must return an absolute URL.
96
+ */
97
+ type BuildUpstreamUrlHook = (mediaHost: string, segments: string[]) => string;
98
+ interface ImageOptimizerRouteContext {
99
+ params: Promise<ImageOptimizerRouteParams>;
100
+ }
101
+ interface ImageOptimizerRouteParams {
102
+ host: string;
103
+ path: string[];
104
+ }
105
+ //#endregion
106
+ //#region src/image-optimizer/create-image-optimizer.d.ts
107
+ /**
108
+ * Creates a Next.js custom image `loader` and a proxy route-handler `GET`
109
+ * that together let an image CDN in front of the storefront (the Alokai
110
+ * Image Optimizer)
111
+ * optimize media-host images.
112
+ *
113
+ * The loader rewrites `src` values matching a configured media host to
114
+ * `/img-proxy/{key}/{path}?width=&quality=&format=auto`; the route handler
115
+ * proxies that path back to the media host and streams the body so the CDN
116
+ * can transform it on the way to the browser. Non-matching `src` values are
117
+ * returned unchanged.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * // config/image-optimizer.ts - the single place the config exists; the
122
+ * // default export lets `images.loaderFile` point straight at this file
123
+ * import { createImageOptimizer } from "@vue-storefront/next";
124
+ *
125
+ * export const { GET, loader } = createImageOptimizer({ hosts: { ct: {} } });
126
+ *
127
+ * export default loader;
128
+ *
129
+ * // app/img-proxy/[host]/[...path]/route.ts
130
+ * export { GET } from "@/config/image-optimizer";
131
+ * ```
132
+ */
133
+ declare function createImageOptimizer(config: CreateImageOptimizerConfig): ImageOptimizer;
134
+ //#endregion
6
135
  //#region src/logger/types.d.ts
7
136
  type LogVerbosity = "alert" | "critical" | "debug" | "emergency" | "error" | "info" | "notice" | "warning";
8
137
  type LoggerOptions = Partial<{
@@ -176,4 +305,4 @@ type DefineSdkModule = (context: InjectedContext) => ReturnType<typeof buildModu
176
305
  */
177
306
  declare function defineSdkModule<TModuleDefinition extends DefineSdkModule>(moduleDefinition: TModuleDefinition): TModuleDefinition;
178
307
  //#endregion
179
- export { type CreateSdkOptions, createAlokaiMiddleware, createLogger, createSdk, defineGetConfigSwitcherHeader, defineSdkConfig, defineSdkModule, env, resolveSdkOptions };
308
+ 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
@@ -18,6 +18,231 @@ const createLogger = (options) => {
18
18
  return injectMetadata(LoggerFactory.create(LoggerType.ConsolaGcp, options), { alokai: { context: "storefront" } });
19
19
  };
20
20
  //#endregion
21
+ //#region src/image-optimizer/constants.ts
22
+ /**
23
+ * URL prefix the loader emits and the route folder must use:
24
+ * `app/img-proxy/[host]/[...path]/route.ts`.
25
+ */
26
+ const IMG_PROXY_PREFIX = "/img-proxy";
27
+ /**
28
+ * Cache-Control applied to successful (2xx) upstream responses unless
29
+ * overridden per host via `cacheControl`.
30
+ */
31
+ const DEFAULT_CACHE_CONTROL = "public, max-age=3600, s-maxage=86400";
32
+ /**
33
+ * Cache-Control applied to every non-2xx or failed response so errors are
34
+ * never cached by the browser or the CDN.
35
+ */
36
+ const ERROR_CACHE_CONTROL = "no-store, no-cache, must-revalidate";
37
+ const variants = {
38
+ default: {
39
+ buildUpstreamUrl: (mediaHost, segments) => `${mediaHost}/${joinEncoded(segments)}`,
40
+ encodePath: (pathname) => pathname === "" ? void 0 : pathname
41
+ },
42
+ sapcc: {
43
+ buildUpstreamUrl: (mediaHost, segments) => {
44
+ const [context = "", ...path] = segments;
45
+ return `${mediaHost}/${joinEncoded(path)}?context=${encodeURIComponent(context)}`;
46
+ },
47
+ encodePath: (pathname, searchParams) => {
48
+ var _pathname$split$pop;
49
+ const context = searchParams.get("context");
50
+ if (!context) return;
51
+ const hasExtension = ((_pathname$split$pop = pathname.split("/").pop()) !== null && _pathname$split$pop !== void 0 ? _pathname$split$pop : "").includes(".");
52
+ const separator = pathname.endsWith("/") ? "" : "/";
53
+ const safePathname = hasExtension ? pathname : `${pathname}${separator}image.png`;
54
+ return `/${encodeURIComponent(context)}${safePathname}`;
55
+ }
56
+ }
57
+ };
58
+ function joinEncoded(segments) {
59
+ return segments.map((segment) => encodeURIComponent(segment)).join("/");
60
+ }
61
+ //#endregion
62
+ //#region src/image-optimizer/resolve-hosts.ts
63
+ /**
64
+ * Normalizes the `hosts` config into an ordered list of fully-resolved host
65
+ * configs. Throws on misconfiguration so mistakes surface at module init
66
+ * instead of as silently unoptimized images.
67
+ *
68
+ * @internal
69
+ */
70
+ function resolveHosts(hosts) {
71
+ return Object.entries(hosts).map(([key, config]) => resolveHost(key, config));
72
+ }
73
+ /**
74
+ * Derives the conventional env variable name from a host key, e.g.
75
+ * `ct` -> `NEXT_PUBLIC_CT_MEDIA_HOST`, `my-cms` -> `NEXT_PUBLIC_MY_CMS_MEDIA_HOST`.
76
+ *
77
+ * @internal
78
+ */
79
+ function deriveMediaHostEnvName(hostKey) {
80
+ return `NEXT_PUBLIC_${hostKey.replaceAll("-", "_").toUpperCase()}_MEDIA_HOST`;
81
+ }
82
+ const HOST_KEY_PATTERN = /^(?=.*[a-z])[a-z0-9-]+$/;
83
+ const ENV_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
84
+ function resolveHost(key, config) {
85
+ var _config$mediaHostEnvN, _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 mediaHostEnvName = (_config$mediaHostEnvN = config.mediaHostEnvName) !== null && _config$mediaHostEnvN !== void 0 ? _config$mediaHostEnvN : deriveMediaHostEnvName(key);
88
+ if (!ENV_NAME_PATTERN.test(mediaHostEnvName)) throw new Error(`Invalid mediaHostEnvName "${mediaHostEnvName}" for host "${key}". Pass the NAME of the env variable (e.g. "NEXT_PUBLIC_CT_MEDIA_HOST"), not its value.`);
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
97
+ };
98
+ }
99
+ //#endregion
100
+ //#region \0@oxc-project+runtime@0.115.0/helpers/asyncToGenerator.js
101
+ function asyncGeneratorStep(n, t, e, r, o, a, c) {
102
+ try {
103
+ var i = n[a](c), u = i.value;
104
+ } catch (n) {
105
+ e(n);
106
+ return;
107
+ }
108
+ i.done ? t(u) : Promise.resolve(u).then(r, o);
109
+ }
110
+ function _asyncToGenerator(n) {
111
+ return function() {
112
+ var t = this, e = arguments;
113
+ return new Promise(function(r, o) {
114
+ var a = n.apply(t, e);
115
+ function _next(n) {
116
+ asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
117
+ }
118
+ function _throw(n) {
119
+ asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
120
+ }
121
+ _next(void 0);
122
+ });
123
+ };
124
+ }
125
+ //#endregion
126
+ //#region src/image-optimizer/create-image-optimizer.ts
127
+ /**
128
+ * Creates a Next.js custom image `loader` and a proxy route-handler `GET`
129
+ * that together let an image CDN in front of the storefront (the Alokai
130
+ * Image Optimizer)
131
+ * optimize media-host images.
132
+ *
133
+ * The loader rewrites `src` values matching a configured media host to
134
+ * `/img-proxy/{key}/{path}?width=&quality=&format=auto`; the route handler
135
+ * proxies that path back to the media host and streams the body so the CDN
136
+ * can transform it on the way to the browser. Non-matching `src` values are
137
+ * returned unchanged.
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * // config/image-optimizer.ts - the single place the config exists; the
142
+ * // default export lets `images.loaderFile` point straight at this file
143
+ * import { createImageOptimizer } from "@vue-storefront/next";
144
+ *
145
+ * export const { GET, loader } = createImageOptimizer({ hosts: { ct: {} } });
146
+ *
147
+ * export default loader;
148
+ *
149
+ * // app/img-proxy/[host]/[...path]/route.ts
150
+ * export { GET } from "@/config/image-optimizer";
151
+ * ```
152
+ */
153
+ function createImageOptimizer(config) {
154
+ if (Object.keys(config.hosts).length === 0) throw new Error([
155
+ "createImageOptimizer requires at least one host in \"hosts\".",
156
+ "Add the media hosts whose images the loader should optimize, e.g.:",
157
+ "",
158
+ " createImageOptimizer({",
159
+ " hosts: {",
160
+ " ct: {},",
161
+ " sapcc: { variant: \"sapcc\" },",
162
+ " },",
163
+ " });",
164
+ "",
165
+ "Each key becomes a URL segment and derives the env variable holding",
166
+ "the media host URL, e.g. \"ct\" reads NEXT_PUBLIC_CT_MEDIA_HOST."
167
+ ].join("\n"));
168
+ const hosts = resolveHosts(config.hosts);
169
+ function loader({ quality, src, width }) {
170
+ for (const host of hosts) {
171
+ const mediaHost = env(host.mediaHostEnvName);
172
+ if (!mediaHost) {
173
+ getLogger().warning(`${host.mediaHostEnvName} is not defined, skipping image optimization for host "${host.key}".`);
174
+ continue;
175
+ }
176
+ const normalizedHost = mediaHost.replace(/\/$/, "");
177
+ if (!matchesMediaHost(src, normalizedHost)) continue;
178
+ if (host.loader) return host.loader({
179
+ quality,
180
+ src,
181
+ width
182
+ });
183
+ const localPath = src.slice(normalizedHost.length);
184
+ const queryIndex = localPath.indexOf("?");
185
+ const pathname = queryIndex === -1 ? localPath : localPath.slice(0, queryIndex);
186
+ const queryString = queryIndex === -1 ? "" : localPath.slice(queryIndex + 1);
187
+ const proxyPath = host.encodePath(pathname, new URLSearchParams(queryString));
188
+ if (proxyPath === void 0) return src;
189
+ return `${IMG_PROXY_PREFIX}/${host.key}${proxyPath}?width=${width}&quality=${quality || 85}&format=auto`;
190
+ }
191
+ return src;
192
+ }
193
+ function GET(_x, _x2) {
194
+ return _GET.apply(this, arguments);
195
+ }
196
+ function _GET() {
197
+ _GET = _asyncToGenerator(function* (_request, { params }) {
198
+ var _env, _upstream$headers$get;
199
+ const { host: hostKey, path } = yield params;
200
+ const host = hosts.find((entry) => entry.key === hostKey);
201
+ if (!host) return errorResponse(`Unknown image proxy host "${hostKey}"`, 404);
202
+ if (path.length === 0 || path.some(isUnsafeSegment)) return errorResponse("Invalid image path", 400);
203
+ const mediaHost = (_env = env(host.mediaHostEnvName)) === null || _env === void 0 ? void 0 : _env.replace(/\/$/, "");
204
+ if (!mediaHost) return errorResponse(`${host.mediaHostEnvName} is not defined`, 500);
205
+ let upstream;
206
+ try {
207
+ upstream = yield fetch(host.buildUpstreamUrl(mediaHost, path));
208
+ } catch (error) {
209
+ getLogger().error(`Failed to fetch upstream image for host "${hostKey}": ${String(error)}`);
210
+ return errorResponse("Failed to fetch upstream image", 502);
211
+ }
212
+ return new Response(upstream.body, {
213
+ headers: {
214
+ "cache-control": upstream.ok ? host.cacheControl : ERROR_CACHE_CONTROL,
215
+ "content-type": (_upstream$headers$get = upstream.headers.get("content-type")) !== null && _upstream$headers$get !== void 0 ? _upstream$headers$get : "application/octet-stream"
216
+ },
217
+ status: upstream.status
218
+ });
219
+ });
220
+ return _GET.apply(this, arguments);
221
+ }
222
+ return {
223
+ GET,
224
+ loader
225
+ };
226
+ }
227
+ let logger;
228
+ function getLogger() {
229
+ var _logger;
230
+ (_logger = logger) !== null && _logger !== void 0 || (logger = createLogger());
231
+ return logger;
232
+ }
233
+ function matchesMediaHost(src, normalizedHost) {
234
+ return src === normalizedHost || src.startsWith(`${normalizedHost}/`) || src.startsWith(`${normalizedHost}?`);
235
+ }
236
+ function isUnsafeSegment(segment) {
237
+ return segment === "" || segment === "." || segment === "..";
238
+ }
239
+ function errorResponse(message, status) {
240
+ return new Response(message, {
241
+ headers: { "cache-control": ERROR_CACHE_CONTROL },
242
+ status
243
+ });
244
+ }
245
+ //#endregion
21
246
  //#region src/middleware.ts
22
247
  /**
23
248
  * Creates an Alokai middleware wrapper that adds pathname information to request headers.
@@ -62,32 +287,6 @@ const defaultMethodsRequestConfig = {
62
287
  } }
63
288
  };
64
289
  //#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
290
  //#region src/sdk/helpers/resolveDynamicContext.ts
92
291
  const BLACKLISTED_HEADERS = new Set(["host"]);
93
292
  function isAppRouterHeaders(headers) {
@@ -234,4 +433,4 @@ function buildModules(modules) {
234
433
  return (context) => Object.fromEntries(Object.entries(modules).map(([key, module]) => [key, module(context)]));
235
434
  }
236
435
  //#endregion
237
- export { createAlokaiMiddleware, createLogger, createSdk, defineGetConfigSwitcherHeader, defineSdkConfig, defineSdkModule, env, resolveSdkOptions };
436
+ export { createAlokaiMiddleware, createImageOptimizer, createLogger, createSdk, defineGetConfigSwitcherHeader, defineSdkConfig, defineSdkModule, env, resolveSdkOptions };
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.3",
10
10
  "exports": {
11
11
  ".": {
12
12
  "types": "./dist/index.d.mts",
@@ -31,11 +31,11 @@
31
31
  "version": "cp CHANGELOG.md ../../docs/enterprise/content/storefront/6.change-log/next.md"
32
32
  },
33
33
  "dependencies": {
34
- "@alokai/instrumentation-next-component": "1.0.3",
34
+ "@alokai/instrumentation-next-component": "1.0.4-next.0",
35
35
  "zustand": "^4.5.4"
36
36
  },
37
37
  "devDependencies": {
38
- "@alokai/connect": "^2.1.0",
38
+ "@alokai/connect": "2.3.0-next.7",
39
39
  "@shared/typescript-config": "1.0.0",
40
40
  "@types/react": "^19",
41
41
  "@types/react-dom": "^19",
@@ -50,13 +50,12 @@
50
50
  "vitest": "4.0.18"
51
51
  },
52
52
  "peerDependencies": {
53
- "@alokai/connect": "^2.1.0",
53
+ "@alokai/connect": "2.3.0-next.7",
54
54
  "next": "^16.0.0",
55
55
  "react": "^19.0.0"
56
56
  },
57
57
  "engines": {
58
- "npm": ">=8.0.0",
59
- "node": ">=18.0.0"
58
+ "node": "^20.10.0 || >=22.14.0"
60
59
  },
61
60
  "publishConfig": {
62
61
  "access": "public"