@doswiftly/storefront-sdk 22.7.0 → 22.8.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## 22.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 61a2841: Fix: images imported in code are optimized again when your build serves static assets from a CDN domain.
8
+
9
+ An image you import in code (`import hero from './hero.webp'`) becomes a content-hashed file under `/_next/static/media/`. When your build serves static assets from a CDN domain (`assetPrefix` / `getAssetPrefix()`), Next.js rewrites the `<Image>` `src` to an absolute CDN URL before the loader runs. The loader did not recognize that form, so every `srcset` entry pointed at the full-size original — no resizing or format negotiation.
10
+
11
+ The loader now recognizes code-imported images in that absolute form and routes them through the image CDN, so each `srcset` width is resized again — the same behavior as a build without `assetPrefix`. Product images and `public/` images were never affected.
12
+
13
+ No API changes — update the package and redeploy.
14
+
15
+ (`@doswiftly/storefront-operations` is version-synced with the SDK; no operation changes.)
16
+
17
+ ## 22.8.0
18
+
19
+ ### Minor Changes
20
+
21
+ - 9039ba1: Add `getAssetPrefix()` for serving your build's static assets from a CDN domain.
22
+
23
+ **Why**: A build's JavaScript, CSS, and font files (`/_next/static/*`) are content-hashed and immutable. Serving them from a CDN domain — rather than your app origin — makes them cache-friendly and keeps them off your storefront server's request path.
24
+
25
+ **Additive (backward-compatible)**: a new export from `@doswiftly/storefront-sdk/next`. Nothing changes until you opt in.
26
+
27
+ **Usage** — add two lines to `next.config`:
28
+
29
+ ```ts
30
+ // next.config.ts
31
+ import { getAssetPrefix } from "@doswiftly/storefront-sdk/next";
32
+
33
+ const nextConfig = {
34
+ assetPrefix: getAssetPrefix(),
35
+ crossOrigin: "anonymous",
36
+ // ...your existing config
37
+ };
38
+ export default nextConfig;
39
+ ```
40
+
41
+ `getAssetPrefix()` returns the CDN base injected by the deploy pipeline (namespaced to your shop), or `undefined` in development and on deploys without a CDN configured — so `next dev` and un-provisioned deploys keep serving assets from the app origin. Pair it with `crossOrigin: 'anonymous'`: the assets then load cross-origin and the CDN sends the matching CORS header (required for fonts).
42
+
43
+ **Migration checklist for existing storefronts**:
44
+ - [ ] Add `assetPrefix: getAssetPrefix()` and `crossOrigin: 'anonymous'` to `next.config`.
45
+ - [ ] Redeploy — the next build's static assets are served from the CDN.
46
+
47
+ (`@doswiftly/storefront-operations` is version-synced with the SDK; no operation changes in this release.)
48
+
3
49
  ## 22.7.0
4
50
 
5
51
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/core/image.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,gFAAgF;IAChF,GAAG,EAAE,MAAM,CAAC;IACZ,uCAAuC;IACvC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,eAAe;IACf,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,2GAA2G;IAC3G,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAMD;;;GAGG;AACH,eAAO,MAAM,kBAAkB,6BAA6B,CAAC;AAE7D,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,2GAA2G;IAC3G,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,uDAAwD,CAAC;AA2C9F,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,iBAAiB,EACzB,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnC,MAAM,CAwDR;AAOD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAiD5F"}
1
+ {"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/core/image.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,gFAAgF;IAChF,GAAG,EAAE,MAAM,CAAC;IACZ,uCAAuC;IACvC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,eAAe;IACf,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,2GAA2G;IAC3G,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAMD;;;GAGG;AACH,eAAO,MAAM,kBAAkB,6BAA6B,CAAC;AAE7D,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,2GAA2G;IAC3G,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,uDAAwD,CAAC;AAmF9F,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,iBAAiB,EACzB,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnC,MAAM,CAiER;AAOD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAiD5F"}
@@ -59,16 +59,54 @@ const LOADER_IMAGE_EXTENSIONS = /\.(png|jpe?g|webp|avif|gif)$/i;
59
59
  * Path prefix of Next.js build-output **media** (images imported in code, e.g.
60
60
  * `import hero from './hero.webp'` → content-hashed bundle). UNLIKE the rest of `/_next/*`
61
61
  * (JS/CSS chunks, left untouched), media images ARE routed through the resize CDN: the deploy
62
- * mirrors them to `s/{shopId}/_next-media/{suffix}`. The content hash in the filename is the
63
- * version, so the CDN key is immutable (no `?v=` cache-bust).
62
+ * mirrors them to `s/{shopId}/_next/static/media/{suffix}` (the path is kept 1:1). The content
63
+ * hash in the filename is the version, so the CDN key is immutable (no `?v=` cache-bust).
64
64
  *
65
- * @sync-with backend deploy mirror (`_next/static/media/` prefix `_next-media/` R2 key)
65
+ * @sync-with backend deploy mirror (`_next/static/media/` path mirrored 1:1 into the R2 key)
66
66
  */
67
67
  const NEXT_STATIC_MEDIA_PREFIX = '/_next/static/media/';
68
68
  /** Suffix after {@link NEXT_STATIC_MEDIA_PREFIX}, or `null` if `pathname` is not a Next media path. */
69
69
  function nextStaticMediaSuffix(pathname) {
70
70
  return pathname.startsWith(NEXT_STATIC_MEDIA_PREFIX) ? pathname.slice(NEXT_STATIC_MEDIA_PREFIX.length) : null;
71
71
  }
72
+ /** Scheme + authority of an absolute http(s) URL — everything up to the first path `/`. */
73
+ const ABSOLUTE_URL_AUTHORITY = /^https?:\/\/[^/]+/i;
74
+ /**
75
+ * Suffix after this shop's `/s/{shopId}/_next/static/media/` when `src` is an ABSOLUTE URL — a
76
+ * code-imported image whose `src` carries the storefront's Next.js `assetPrefix`
77
+ * (`{assetPrefixBase}/s/{shopId}/_next/static/media/{suffix}`, which Next prepends before the loader
78
+ * runs). `null` for relative srcs, other shops' assets, and foreign hosts. Anchored on `/s/{shopId}/`
79
+ * so a foreign host that merely embeds the marker mid-path is rejected; matched on path structure, not
80
+ * host — the asset-prefix CDN host is not part of the loader config, only the resize base is.
81
+ *
82
+ * The path is taken from the RAW `src` (strip scheme/authority + query/hash), NOT via `URL.pathname`:
83
+ * that percent-encodes the path, which would double-encode in {@link buildNextMediaCdnUrl}. The
84
+ * root-relative counterpart {@link nextStaticMediaSuffix} likewise feeds a raw path, so both `src`
85
+ * forms of one import — relative (no `assetPrefix`) and absolute (with it) — encode identically.
86
+ */
87
+ function absoluteNextMediaSuffix(src, shopId) {
88
+ // Fast reject before any allocation: must be an absolute http(s) URL that mentions the media segment.
89
+ if (!ABSOLUTE_URL_AUTHORITY.test(src) || !src.includes(NEXT_STATIC_MEDIA_PREFIX))
90
+ return null;
91
+ const path = src.replace(ABSOLUTE_URL_AUTHORITY, '').replace(/[?#].*$/, '');
92
+ const prefix = `/s/${shopId}${NEXT_STATIC_MEDIA_PREFIX}`;
93
+ return path.startsWith(prefix) ? path.slice(prefix.length) : null;
94
+ }
95
+ /**
96
+ * Image-CDN URL for a Next build-output media image (content-hashed → the hash IS the version, so the
97
+ * key is immutable: only the per-srcset `width`, no `?v=`). The same key shape the deploy mirror
98
+ * writes, so the resize CDN finds the file. Shared by both `src` forms of one import: root-relative
99
+ * ({@link nextStaticMediaSuffix}) and absolute `assetPrefix` ({@link absoluteNextMediaSuffix}).
100
+ *
101
+ * `encodeURIComponent` keeps `..` (unreserved) — harmless while the image CDN is unsigned + public;
102
+ * if HMAC signing is ever added, strip `..` segments here AND in the public/ branch.
103
+ *
104
+ * @sync-with backend deploy mirror (`_next/static/media/` mirrored 1:1 into the image-CDN key)
105
+ */
106
+ function buildNextMediaCdnUrl(base, shopId, suffix, width) {
107
+ const encoded = suffix.split('/').map(encodeURIComponent).join('/');
108
+ return `${base}/s/${shopId}/_next/static/media/${encoded}?width=${width}`;
109
+ }
72
110
  export function buildImageLoaderUrl(config, args) {
73
111
  // `||` (not `??`): an empty injected base must still fall back to the platform default.
74
112
  const base = (config.cdnBase || IMAGE_CDN_BASE_URL).replace(/\/+$/, '');
@@ -84,6 +122,18 @@ export function buildImageLoaderUrl(config, args) {
84
122
  return src;
85
123
  }
86
124
  }
125
+ // (1b) Absolute build-output media URL — a code-imported image whose `src` carries the storefront's
126
+ // next.config `assetPrefix` (`{base}/s/{shopId}/_next/static/media/...`, prepended by Next
127
+ // before this loader). Routed to the SAME image-CDN key as the root-relative case (2a) — the two
128
+ // are just the assetPrefix / no-assetPrefix forms of one import. Fonts (`.woff2`) share
129
+ // `_next/static/media/` but are not images, so the extension gate leaves them to load raw from
130
+ // the asset CDN (they are not mirrored to the resize CDN).
131
+ if (config.shopId) {
132
+ const mediaSuffix = absoluteNextMediaSuffix(src, config.shopId);
133
+ if (mediaSuffix !== null && LOADER_IMAGE_EXTENSIONS.test(mediaSuffix)) {
134
+ return buildNextMediaCdnUrl(base, config.shopId, mediaSuffix, width);
135
+ }
136
+ }
87
137
  // (2) Local root-relative image — NOT protocol-relative (`//host`). Two sub-cases:
88
138
  // (2a) a Next build-output media image (`/_next/static/media/*`), and (2b) a `public/` image.
89
139
  if (src.startsWith('/') && !src.startsWith('//')) {
@@ -96,14 +146,10 @@ export function buildImageLoaderUrl(config, args) {
96
146
  // extension gate is the safety that keeps framework build chunks out of the resize CDN.
97
147
  if (!LOADER_IMAGE_EXTENSIONS.test(pathname))
98
148
  return src;
99
- // (2a) Next build-output media image. The content hash in the filename IS the version, so
100
- // the CDN key is immutable → no `?v=`. The deploy mirrors these to `s/{shopId}/_next-media/`.
149
+ // (2a) Next build-output media image same image-CDN key as the absolute (assetPrefix) form (1b).
101
150
  const mediaSuffix = nextStaticMediaSuffix(pathname);
102
151
  if (mediaSuffix !== null) {
103
- // `encodeURIComponent` keeps `..` (unreserved) — harmless while the image CDN is unsigned +
104
- // public; if HMAC signing is ever added, strip `..` segments here AND in the public/ branch.
105
- const encoded = mediaSuffix.split('/').map(encodeURIComponent).join('/');
106
- return `${base}/s/${config.shopId}/_next-media/${encoded}?width=${width}`;
152
+ return buildNextMediaCdnUrl(base, config.shopId, mediaSuffix, width);
107
153
  }
108
154
  // Any other framework build output (hashed/immutable, NOT mirrored) → untouched.
109
155
  const buildPrefixes = config.buildAssetPrefixes ?? FRAMEWORK_BUILD_PREFIXES;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Next.js `assetPrefix` for DoSwiftly storefronts.
3
+ *
4
+ * Wire it once:
5
+ * ```ts
6
+ * // next.config.ts
7
+ * import { getAssetPrefix } from '@doswiftly/storefront-sdk/next';
8
+ * const nextConfig = { assetPrefix: getAssetPrefix(), crossOrigin: 'anonymous' };
9
+ * ```
10
+ *
11
+ * Your static assets (`/_next/static/*` — JavaScript, CSS, and fonts) are then served from your
12
+ * storefront's own CDN domain instead of the app origin: cache-friendly, immutable, and off the
13
+ * request path of your storefront server. The CDN base is injected by the DoSwiftly deploy
14
+ * pipeline — you don't configure it.
15
+ *
16
+ * Returns `undefined` in development (so `doswiftly dev` serves assets locally for fast refresh) and
17
+ * whenever the pipeline hasn't injected a CDN base (so an un-provisioned deploy keeps serving
18
+ * assets from the app origin). Either way there is no setup and nothing to break.
19
+ *
20
+ * Pair it with `crossOrigin: 'anonymous'`: assets then load cross-origin and the CDN sends the
21
+ * matching CORS header (required for fonts).
22
+ */
23
+ export declare function getAssetPrefix(): string | undefined;
24
+ //# sourceMappingURL=asset-prefix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-prefix.d.ts","sourceRoot":"","sources":["../../src/next/asset-prefix.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,IAAI,MAAM,GAAG,SAAS,CAKnD"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Next.js `assetPrefix` for DoSwiftly storefronts.
3
+ *
4
+ * Wire it once:
5
+ * ```ts
6
+ * // next.config.ts
7
+ * import { getAssetPrefix } from '@doswiftly/storefront-sdk/next';
8
+ * const nextConfig = { assetPrefix: getAssetPrefix(), crossOrigin: 'anonymous' };
9
+ * ```
10
+ *
11
+ * Your static assets (`/_next/static/*` — JavaScript, CSS, and fonts) are then served from your
12
+ * storefront's own CDN domain instead of the app origin: cache-friendly, immutable, and off the
13
+ * request path of your storefront server. The CDN base is injected by the DoSwiftly deploy
14
+ * pipeline — you don't configure it.
15
+ *
16
+ * Returns `undefined` in development (so `doswiftly dev` serves assets locally for fast refresh) and
17
+ * whenever the pipeline hasn't injected a CDN base (so an un-provisioned deploy keeps serving
18
+ * assets from the app origin). Either way there is no setup and nothing to break.
19
+ *
20
+ * Pair it with `crossOrigin: 'anonymous'`: assets then load cross-origin and the CDN sends the
21
+ * matching CORS header (required for fonts).
22
+ */
23
+ export function getAssetPrefix() {
24
+ // Development must serve assets from the dev server (fast refresh) — never the CDN.
25
+ if (process.env.NODE_ENV === "development")
26
+ return undefined;
27
+ // Literal member access (read at build time inside next.config). Empty / unset → app origin.
28
+ return process.env.DOSWIFTLY_ASSET_PREFIX || undefined;
29
+ }
@@ -6,4 +6,5 @@
6
6
  * core stays portable to Node, Edge, Deno, and Bun.
7
7
  */
8
8
  export { createImageLoader, type NextImageLoaderArgs } from './image-loader';
9
+ export { getAssetPrefix } from './asset-prefix';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC"}
@@ -6,6 +6,7 @@
6
6
  * core stays portable to Node, Edge, Deno, and Bun.
7
7
  */
8
8
  export { createImageLoader } from './image-loader';
9
+ export { getAssetPrefix } from './asset-prefix';
9
10
  // The pure builder + its config/base (`buildImageLoaderUrl`, `IMAGE_CDN_BASE_URL`,
10
11
  // `ImageLoaderConfig`) are framework-agnostic and live on the core entry
11
12
  // (`@doswiftly/storefront-sdk`) — import them from there, not from `/next`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doswiftly/storefront-sdk",
3
- "version": "22.7.0",
3
+ "version": "22.8.1",
4
4
  "description": "Storefront runtime SDK for DoSwiftly Commerce — layered transport, middleware pipeline, React providers, Zustand stores, cache strategies. 0 runtime dependencies in core.",
5
5
  "type": "module",
6
6
  "sideEffects": false,