@vue-storefront/nuxt 10.0.1-rc.0 → 11.0.0-next.5

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/module.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
+ import { ImageOptimizerConfig } from '../dist/runtime/image-optimizer/types.js';
2
3
 
3
4
  interface MiddlewareConfig {
4
5
  /**
@@ -33,6 +34,12 @@ type LoggerOptions = Partial<{
33
34
  verbosity: LogVerbosity;
34
35
  }>;
35
36
  interface AlokaiModuleOptions {
37
+ /**
38
+ * Opt-in Alokai Image Optimizer config. When set with at least one host, the
39
+ * module registers an `@nuxt/image` provider and a `/img-proxy` route that
40
+ * proxy matching media-host images through the optimizer CDN.
41
+ */
42
+ imageOptimizer?: ImageOptimizerConfig;
36
43
  logger?: LoggerOptions;
37
44
  middleware: MiddlewareConfig;
38
45
  multistore?: MultistoreConfig;
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  },
5
5
  "configKey": "alokai",
6
6
  "name": "@vue-storefront/nuxt",
7
- "version": "10.0.1-rc.0",
7
+ "version": "11.0.0-next.5",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,7 +1,9 @@
1
- import { defineNuxtModule, createResolver, installModule, addTypeTemplate, addImports, addTemplate, addPluginTemplate, addImportsSources, addServerHandler } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, installModule, addServerHandler, addTypeTemplate, addImports, addTemplate, addPluginTemplate, addImportsSources } from '@nuxt/kit';
2
2
  import { defu } from 'defu';
3
3
  import { genInlineTypeImport } from 'knitwork';
4
4
  import { readFileSync } from 'node:fs';
5
+ import { IMG_PROXY_PREFIX } from '../dist/runtime/image-optimizer/constants.js';
6
+ import { resolveHosts } from '../dist/runtime/image-optimizer/resolve-hosts.js';
5
7
 
6
8
  const RUNTIME_CONFIG = {
7
9
  public: {}
@@ -38,6 +40,30 @@ const module$1 = defineNuxtModule({
38
40
  nuxt.options.runtimeConfig.public?.alokai,
39
41
  options
40
42
  );
43
+ if (options.imageOptimizer && Object.keys(options.imageOptimizer.hosts).length > 0) {
44
+ const resolvedHosts = resolveHosts(options.imageOptimizer.hosts);
45
+ for (const host of resolvedHosts) {
46
+ nuxt.options.runtimeConfig.public[host.runtimeConfigKey] = nuxt.options.runtimeConfig.public[host.runtimeConfigKey] ?? "";
47
+ }
48
+ nuxt.options.runtimeConfig.public.alokaiImageOptimizer = { hosts: resolvedHosts };
49
+ const nuxtOptions = nuxt.options;
50
+ nuxtOptions.image = {
51
+ ...nuxtOptions.image,
52
+ provider: "alokai",
53
+ providers: {
54
+ ...nuxtOptions.image?.providers,
55
+ alokai: {
56
+ provider: localResolver.resolve(
57
+ "./runtime/image-optimizer/provider"
58
+ )
59
+ }
60
+ }
61
+ };
62
+ addServerHandler({
63
+ handler: localResolver.resolve("./runtime/image-optimizer/handler"),
64
+ route: `${IMG_PROXY_PREFIX}/:host/**:path`
65
+ });
66
+ }
41
67
  Object.assign(
42
68
  nuxt.options.appConfig,
43
69
  defu(nuxt.options.appConfig, {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * URL prefix the provider emits and the server route is mounted on:
3
+ * `/img-proxy/:host/**:path`.
4
+ */
5
+ export declare const IMG_PROXY_PREFIX = "/img-proxy";
6
+ /**
7
+ * Cache-Control applied to successful (2xx) upstream responses unless
8
+ * overridden per host via `cacheControl`.
9
+ */
10
+ export declare const DEFAULT_CACHE_CONTROL = "public, max-age=3600, s-maxage=86400";
11
+ /**
12
+ * Cache-Control applied to every non-2xx or failed response so errors are
13
+ * never cached by the browser or the CDN.
14
+ */
15
+ export declare const ERROR_CACHE_CONTROL = "no-store, no-cache, must-revalidate";
16
+ export declare const DEFAULT_QUALITY = 85;
@@ -0,0 +1,4 @@
1
+ export const IMG_PROXY_PREFIX = "/img-proxy";
2
+ export const DEFAULT_CACHE_CONTROL = "public, max-age=3600, s-maxage=86400";
3
+ export const ERROR_CACHE_CONTROL = "no-store, no-cache, must-revalidate";
4
+ export const DEFAULT_QUALITY = 85;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Proxy route for `/img-proxy/:host/**:path`. Reconstructs the upstream media
3
+ * URL the provider encoded and streams the response body back so the Alokai
4
+ * Image Optimizer CDN can transform it on the way to the browser.
5
+ */
6
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Response>>;
7
+ export default _default;
@@ -0,0 +1,65 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ import { defineEventHandler, getRouterParam } from "h3";
3
+ import { ERROR_CACHE_CONTROL } from "./constants.js";
4
+ import { logger } from "./logger.js";
5
+ import { variants } from "./variants.js";
6
+ export default defineEventHandler(async (event) => {
7
+ const hostKey = getRouterParam(event, "host");
8
+ const rawPath = getRouterParam(event, "path") ?? "";
9
+ const publicConfig = useRuntimeConfig(event).public;
10
+ const host = (publicConfig.alokaiImageOptimizer?.hosts ?? []).find(
11
+ (entry) => entry.key === hostKey
12
+ );
13
+ if (!host) {
14
+ return errorResponse(`Unknown image proxy host "${hostKey}"`, 404);
15
+ }
16
+ const segments = decodeSegments(rawPath);
17
+ if (segments === void 0 || segments.length === 0 || segments.some(isUnsafeSegment)) {
18
+ return errorResponse("Invalid image path", 400);
19
+ }
20
+ const mediaHost = publicConfig[host.runtimeConfigKey]?.replace(/\/$/, "");
21
+ if (!mediaHost) {
22
+ return errorResponse(
23
+ `${host.runtimeConfigKey} runtime config is empty`,
24
+ 500
25
+ );
26
+ }
27
+ let upstream;
28
+ try {
29
+ upstream = await fetch(
30
+ variants[host.variant].buildUpstreamUrl(mediaHost, segments),
31
+ { redirect: "manual" }
32
+ );
33
+ } catch (error) {
34
+ logger.error(
35
+ `Failed to fetch upstream image for host "${hostKey}": ${String(error)}`
36
+ );
37
+ return errorResponse("Failed to fetch upstream image", 502);
38
+ }
39
+ if (upstream.status >= 300 && upstream.status < 400) {
40
+ return errorResponse("Upstream redirect is not allowed", 502);
41
+ }
42
+ return new Response(upstream.body, {
43
+ headers: {
44
+ "cache-control": upstream.ok ? host.cacheControl : ERROR_CACHE_CONTROL,
45
+ "content-type": upstream.headers.get("content-type") ?? "application/octet-stream"
46
+ },
47
+ status: upstream.status
48
+ });
49
+ });
50
+ function decodeSegments(rawPath) {
51
+ try {
52
+ return rawPath.split("/").map((segment) => decodeURIComponent(segment));
53
+ } catch {
54
+ return void 0;
55
+ }
56
+ }
57
+ function isUnsafeSegment(segment) {
58
+ return segment === "" || segment === "." || segment === "..";
59
+ }
60
+ function errorResponse(message, status) {
61
+ return new Response(message, {
62
+ headers: { "cache-control": ERROR_CACHE_CONTROL },
63
+ status
64
+ });
65
+ }
@@ -0,0 +1,20 @@
1
+ import type { ResolvedImageOptimizerHost } from "./types.js";
2
+ export interface BuildProxyUrlParams {
3
+ /** Resolved hosts, matched in order; first match wins. */
4
+ hosts: ResolvedImageOptimizerHost[];
5
+ /** Invoked once per host whose media host is unset, before skipping it. */
6
+ onMissingHost?: (host: ResolvedImageOptimizerHost) => void;
7
+ quality?: number | string;
8
+ /** Reads the media host value for a host, or `undefined`/empty when unset. */
9
+ resolveMediaHost: (host: ResolvedImageOptimizerHost) => string | undefined;
10
+ src: string;
11
+ width?: number | string;
12
+ }
13
+ /**
14
+ * Rewrites a media-host `src` to its `/img-proxy/{key}/...` proxy URL so the
15
+ * Alokai Image Optimizer CDN can transform it. Returns a non-matching `src`
16
+ * unchanged. Framework-agnostic so it can be unit-tested in isolation and
17
+ * shared between the `@nuxt/image` provider and any other caller.
18
+ */
19
+ export declare function buildProxyUrl({ hosts, onMissingHost, quality, resolveMediaHost, src, width, }: BuildProxyUrlParams): string;
20
+ export declare function matchesMediaHost(src: string, normalizedHost: string): boolean;
@@ -0,0 +1,44 @@
1
+ import { DEFAULT_QUALITY, IMG_PROXY_PREFIX } from "./constants.js";
2
+ import { variants } from "./variants.js";
3
+ export function buildProxyUrl({
4
+ hosts,
5
+ onMissingHost,
6
+ quality,
7
+ resolveMediaHost,
8
+ src,
9
+ width
10
+ }) {
11
+ for (const host of hosts) {
12
+ const mediaHost = resolveMediaHost(host);
13
+ if (!mediaHost) {
14
+ onMissingHost?.(host);
15
+ continue;
16
+ }
17
+ const normalizedHost = mediaHost.replace(/\/$/, "");
18
+ if (!matchesMediaHost(src, normalizedHost)) {
19
+ continue;
20
+ }
21
+ const localPath = src.slice(normalizedHost.length);
22
+ const queryIndex = localPath.indexOf("?");
23
+ const pathname = queryIndex === -1 ? localPath : localPath.slice(0, queryIndex);
24
+ const queryString = queryIndex === -1 ? "" : localPath.slice(queryIndex + 1);
25
+ const proxyPath = variants[host.variant].encodePath(
26
+ pathname,
27
+ new URLSearchParams(queryString)
28
+ );
29
+ if (proxyPath === void 0) {
30
+ return src;
31
+ }
32
+ const params = new URLSearchParams();
33
+ if (width) {
34
+ params.set("width", String(width));
35
+ }
36
+ params.set("quality", String(quality || DEFAULT_QUALITY));
37
+ params.set("format", "auto");
38
+ return `${IMG_PROXY_PREFIX}/${host.key}${proxyPath}?${params.toString()}`;
39
+ }
40
+ return src;
41
+ }
42
+ export function matchesMediaHost(src, normalizedHost) {
43
+ return src === normalizedHost || src.startsWith(`${normalizedHost}/`) || src.startsWith(`${normalizedHost}?`);
44
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Dedicated Alokai logger for the image optimizer. The provider runs in both
3
+ * the Vue and Nitro contexts, where the module's auto-imported `logger` is not
4
+ * uniformly available, so a self-contained instance keeps logging consistent
5
+ * (and avoids `console.*`) on both sides.
6
+ */
7
+ export declare const logger: import("@alokai/connect/logger").LoggerInterface;
@@ -0,0 +1,2 @@
1
+ import { LoggerFactory, LoggerType } from "@alokai/connect/logger";
2
+ export const logger = LoggerFactory.create(LoggerType.ConsolaGcp);
@@ -0,0 +1,2 @@
1
+ declare const _default: () => import("@nuxt/image").ImageProvider<unknown>;
2
+ export default _default;
@@ -0,0 +1,28 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ import { defineProvider } from "@nuxt/image/runtime";
3
+ import { buildProxyUrl } from "./loader.js";
4
+ import { logger } from "./logger.js";
5
+ const warnedMissingHosts = /* @__PURE__ */ new Set();
6
+ export default defineProvider({
7
+ getImage(src, { modifiers = {} }) {
8
+ const publicConfig = useRuntimeConfig().public;
9
+ const hosts = publicConfig.alokaiImageOptimizer?.hosts ?? [];
10
+ const url = buildProxyUrl({
11
+ hosts,
12
+ onMissingHost: (host) => {
13
+ if (warnedMissingHosts.has(host.runtimeConfigKey)) {
14
+ return;
15
+ }
16
+ warnedMissingHosts.add(host.runtimeConfigKey);
17
+ logger.warning(
18
+ `${host.runtimeConfigKey} runtime config is empty, skipping image optimization for host "${host.key}".`
19
+ );
20
+ },
21
+ quality: modifiers.quality,
22
+ resolveMediaHost: (host) => publicConfig[host.runtimeConfigKey],
23
+ src,
24
+ width: modifiers.width
25
+ });
26
+ return { url };
27
+ }
28
+ });
@@ -0,0 +1,19 @@
1
+ import type { ImageOptimizerHosts, ResolvedImageOptimizerHost } from "./types.js";
2
+ /**
3
+ * Normalizes the `hosts` config into an ordered list of fully-resolved host
4
+ * configs. Throws on misconfiguration so mistakes surface at module init
5
+ * instead of as silently unoptimized images.
6
+ *
7
+ * @internal
8
+ */
9
+ export declare function resolveHosts(hosts: ImageOptimizerHosts): ResolvedImageOptimizerHost[];
10
+ /**
11
+ * Derives the public runtime config key that holds the media host, e.g.
12
+ * `commerce` -> `commerceMediaHost`, `my-cms` -> `myCmsMediaHost`. Nuxt
13
+ * overrides this key at runtime from the matching `NUXT_PUBLIC_{KEY}_MEDIA_HOST`
14
+ * env variable, so the value is configurable per deployment without the env
15
+ * variable name having to be declared anywhere.
16
+ *
17
+ * @internal
18
+ */
19
+ export declare function deriveRuntimeConfigKey(hostKey: string): string;
@@ -0,0 +1,24 @@
1
+ import { DEFAULT_CACHE_CONTROL } from "./constants.js";
2
+ export function resolveHosts(hosts) {
3
+ return Object.entries(hosts).map(([key, config]) => resolveHost(key, config));
4
+ }
5
+ export function deriveRuntimeConfigKey(hostKey) {
6
+ const camelKey = hostKey.split("-").filter(Boolean).map(
7
+ (word, index) => index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)
8
+ ).join("");
9
+ return `${camelKey}MediaHost`;
10
+ }
11
+ const HOST_KEY_PATTERN = /^(?=.*[a-z])[a-z0-9-]+$/;
12
+ function resolveHost(key, config) {
13
+ if (!HOST_KEY_PATTERN.test(key)) {
14
+ throw new Error(
15
+ `Invalid image optimizer host key "${key}". Use lowercase letters, digits and hyphens only - the key becomes a URL segment and a runtime config key.`
16
+ );
17
+ }
18
+ return {
19
+ cacheControl: config.cacheControl ?? DEFAULT_CACHE_CONTROL,
20
+ key,
21
+ runtimeConfigKey: deriveRuntimeConfigKey(key),
22
+ variant: config.variant ?? "default"
23
+ };
24
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Built-in URL scheme. `sapcc` encodes the `?context=` query parameter as a
3
+ * path segment and appends a synthetic filename when the path has none.
4
+ */
5
+ export type ImageOptimizerVariant = "default" | "sapcc";
6
+ export interface ImageOptimizerHostConfig {
7
+ /**
8
+ * Cache-Control for successful (2xx) upstream responses.
9
+ *
10
+ * @default "public, max-age=3600, s-maxage=86400"
11
+ */
12
+ cacheControl?: string;
13
+ /**
14
+ * Built-in URL scheme used to encode and reconstruct the proxied path.
15
+ *
16
+ * @default "default"
17
+ */
18
+ variant?: ImageOptimizerVariant;
19
+ }
20
+ /**
21
+ * Media hosts whose images should be optimized, keyed by host key. The key
22
+ * becomes the `:host` URL segment and the media host value is read at runtime
23
+ * from the matching `NUXT_PUBLIC_{KEY}_MEDIA_HOST` env variable.
24
+ */
25
+ export type ImageOptimizerHosts = Record<string, ImageOptimizerHostConfig>;
26
+ export interface ImageOptimizerConfig {
27
+ hosts: ImageOptimizerHosts;
28
+ }
29
+ /**
30
+ * Fully-resolved per-host config. Serialized into the public runtime config so
31
+ * both the `@nuxt/image` provider and the proxy server route can read it.
32
+ *
33
+ * @internal
34
+ */
35
+ export interface ResolvedImageOptimizerHost {
36
+ cacheControl: string;
37
+ key: string;
38
+ /** Public runtime config key holding the resolved media host value. */
39
+ runtimeConfigKey: string;
40
+ variant: ImageOptimizerVariant;
41
+ }
42
+ /**
43
+ * Shape of `useRuntimeConfig().public` as the image optimizer reads it: the
44
+ * resolved hosts plus the dynamically-keyed media host values.
45
+ *
46
+ * @internal
47
+ */
48
+ export interface ImageOptimizerPublicConfig {
49
+ [runtimeConfigKey: string]: unknown;
50
+ alokaiImageOptimizer?: {
51
+ hosts?: ResolvedImageOptimizerHost[];
52
+ };
53
+ }
54
+ /**
55
+ * Builds the path emitted after `/img-proxy/{key}` on the provider (client)
56
+ * side. Receives the pathname relative to the media host (with leading slash,
57
+ * without query string) and the parsed query string of the original `src`.
58
+ * Returns the proxy path (starting with `/`), or `undefined` to skip proxying
59
+ * and return the original `src` unchanged.
60
+ *
61
+ * @internal
62
+ */
63
+ export type EncodePathHook = (pathname: string, searchParams: URLSearchParams) => string | undefined;
64
+ /**
65
+ * Rebuilds the upstream URL on the server-route side. Receives the media host
66
+ * (without trailing slash) and the decoded path segments. Returns an absolute
67
+ * URL.
68
+ *
69
+ * @internal
70
+ */
71
+ export type BuildUpstreamUrlHook = (mediaHost: string, segments: string[]) => string;
File without changes
@@ -0,0 +1,7 @@
1
+ import type { BuildUpstreamUrlHook, EncodePathHook, ImageOptimizerVariant } from "./types.js";
2
+ interface VariantImplementation {
3
+ buildUpstreamUrl: BuildUpstreamUrlHook;
4
+ encodePath: EncodePathHook;
5
+ }
6
+ export declare const variants: Record<ImageOptimizerVariant, VariantImplementation>;
7
+ export {};
@@ -0,0 +1,29 @@
1
+ const defaultVariant = {
2
+ buildUpstreamUrl: (mediaHost, segments) => `${mediaHost}/${joinEncoded(segments)}`,
3
+ // An empty pathname (src equal to the bare media host) could never match
4
+ // the route, so pass it through instead of emitting a dead URL.
5
+ encodePath: (pathname) => pathname === "" ? void 0 : pathname
6
+ };
7
+ const sapccVariant = {
8
+ buildUpstreamUrl: (mediaHost, segments) => {
9
+ const [context = "", ...path] = segments;
10
+ return `${mediaHost}/${joinEncoded(path)}?context=${encodeURIComponent(context)}`;
11
+ },
12
+ encodePath: (pathname, searchParams) => {
13
+ const context = searchParams.get("context");
14
+ if (!context) {
15
+ return void 0;
16
+ }
17
+ const hasExtension = (pathname.split("/").pop() ?? "").includes(".");
18
+ const separator = pathname.endsWith("/") ? "" : "/";
19
+ const safePathname = hasExtension ? pathname : `${pathname}${separator}image.png`;
20
+ return `/${encodeURIComponent(context)}${safePathname}`;
21
+ }
22
+ };
23
+ export const variants = {
24
+ default: defaultVariant,
25
+ sapcc: sapccVariant
26
+ };
27
+ function joinEncoded(segments) {
28
+ return segments.map((segment) => encodeURIComponent(segment)).join("/");
29
+ }
@@ -42,8 +42,20 @@ export type LoggerOptions = Partial<{
42
42
  includeStackTrace: boolean;
43
43
  }>;
44
44
 
45
+ export type ImageOptimizerVariant = "default" | "sapcc";
46
+
47
+ export interface ImageOptimizerHostConfig {
48
+ variant?: ImageOptimizerVariant;
49
+ cacheControl?: string;
50
+ }
51
+
52
+ export interface ImageOptimizerConfig {
53
+ hosts: Record<string, ImageOptimizerHostConfig>;
54
+ }
55
+
45
56
  export interface AlokaiModuleOptions {
46
57
  middleware: MiddlewareConfig;
47
58
  multistore?: MultistoreConfig;
48
59
  logger?: LoggerOptions;
60
+ imageOptimizer?: ImageOptimizerConfig;
49
61
  }
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@vue-storefront/nuxt",
3
- "version": "10.0.1-rc.0",
3
+ "version": "11.0.0-next.5",
4
4
  "description": "Alokai dedicated features for Nuxt",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/vuestorefront/enterprise.git"
9
+ },
6
10
  "type": "module",
7
11
  "exports": {
8
12
  ".": {
@@ -20,6 +24,7 @@
20
24
  "lint:fix": "eslint --fix",
21
25
  "format": "prettier --write .",
22
26
  "prepare": "nuxi prepare",
27
+ "test:unit": "vitest run",
23
28
  "version": "cp CHANGELOG.md ../../docs/enterprise/content/storefront/6.change-log/nuxt.md"
24
29
  },
25
30
  "dependencies": {
@@ -32,17 +37,25 @@
32
37
  "pinia": "^3.0.4"
33
38
  },
34
39
  "devDependencies": {
35
- "@alokai/connect": "^2.2.0-rc.0",
40
+ "@alokai/connect": "2.3.0-next.7",
36
41
  "@nuxt/devtools": "^3.0.0",
42
+ "@nuxt/image": "^2.0.0",
37
43
  "@nuxt/module-builder": "^1.0.2",
38
44
  "@types/node": "^22.0.0",
39
45
  "eslint": "9.23.0",
40
46
  "nuxt": "4.3.0",
41
47
  "prettier": "3.3.2",
42
- "typescript": "5.6.2"
48
+ "typescript": "5.6.2",
49
+ "vitest": "4.0.18"
43
50
  },
44
51
  "peerDependencies": {
45
- "@alokai/connect": "^2.2.0-rc.0"
52
+ "@alokai/connect": "2.3.0-next.7",
53
+ "@nuxt/image": "^2.0.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@nuxt/image": {
57
+ "optional": true
58
+ }
46
59
  },
47
60
  "publishConfig": {
48
61
  "access": "public"