@akinon/next 2.0.23-beta.0 → 2.0.23

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,10 +1,19 @@
1
1
  # @akinon/next
2
2
 
3
- ## 2.0.23-beta.0
3
+ ## 2.0.23
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - cbbbfd75: ZERO-4376: Bootstrap beta cycle (next-main pre-release motor)
7
+ - 079fc67: ZERO-4522: Normalize searchParams at the entry of server data functions so v2 brand SSR no longer drops query params and no longer collides on a single Redis cache entry.
8
+
9
+ `withSegmentDefaults` converts Next 16's plain searchParams into a `URLSearchParams` instance for v1 brand compatibility. The instance then flowed into `getProductData` / `getCategoryData` / `getListData` / `getSpecialPageData` and broke two things:
10
+
11
+ - `JSON.stringify(new URLSearchParams(...))` returns `"{}"`, so every cache key for the same `pk` collapsed onto one Redis entry. The first user's filtered/variant response was served to every subsequent visitor.
12
+ - `getProductData`'s outbound URL builder used `Object.keys(searchParams).map(...)`, which returns `[]` for `URLSearchParams`. The backend product fetch silently dropped every query param — variant/attribute selections on the PDP never reached the commerce backend, and the add-to-cart button stayed stuck in its loading state.
13
+
14
+ Add `normalizeSearchParams` (`packages/akinon-next/utils/normalize-search-params.ts`) and call it once at the entry of each public data function. Multi-value URLSearchParams keys (`?color=red&color=blue`) collapse to `string[]` so multi-select filters survive. v1 brand pages keep receiving `URLSearchParams` from the HOC — only the internal data layer canonicalizes. No public signature change.
15
+
16
+ Side note: v1 brand PDP requests will now actually forward `?` params to the commerce backend instead of silently dropping them. Any v1 brand that relied on PDP requests ignoring query strings should smoke-test variant fetches.
8
17
 
9
18
  ## 2.0.22
10
19
 
@@ -1,5 +1,6 @@
1
1
  import { GetCategoryResponse, SearchParams } from '../../types';
2
2
  import { generateCommerceSearchParams } from '../../utils';
3
+ import { normalizeSearchParams } from '../../utils/normalize-search-params';
3
4
  import appFetch, { FetchResponseType } from '../../utils/app-fetch';
4
5
  import { category, product } from '../urls';
5
6
  import { Cache, CacheKey } from '../../lib/cache';
@@ -93,6 +94,8 @@ export const getCategoryData = ({
93
94
  searchParams?: SearchParams;
94
95
  headers?: Record<string, string>;
95
96
  }) => {
97
+ searchParams = normalizeSearchParams(searchParams);
98
+
96
99
  return Cache.wrap(
97
100
  CacheKey.Category(pk, searchParams, headers),
98
101
  locale,
@@ -2,6 +2,7 @@ import { Cache, CacheKey } from '../../lib/cache';
2
2
  import { category } from '../urls';
3
3
  import { GetCategoryResponse, SearchParams } from '../../types';
4
4
  import { generateCommerceSearchParams } from '../../utils';
5
+ import { normalizeSearchParams } from '../../utils/normalize-search-params';
5
6
  import appFetch, { FetchResponseType } from '../../utils/app-fetch';
6
7
  import { parse } from 'lossless-json';
7
8
  import logger from '../../utils/log';
@@ -66,6 +67,8 @@ export const getListData = async ({
66
67
  searchParams: SearchParams;
67
68
  headers?: Record<string, string>;
68
69
  }) => {
70
+ searchParams = normalizeSearchParams(searchParams);
71
+
69
72
  return Cache.wrap(
70
73
  CacheKey.List(searchParams, headers),
71
74
  locale,
@@ -2,6 +2,7 @@ import { Cache, CacheKey } from '../../lib/cache';
2
2
  import { product } from '../urls';
3
3
  import { ProductCategoryResult, ProductResult, SearchParams } from '../../types';
4
4
  import appFetch from '../../utils/app-fetch';
5
+ import { normalizeSearchParams } from '../../utils/normalize-search-params';
5
6
  import { ServerVariables } from '../../utils/server-variables';
6
7
  import logger from '../../utils/log';
7
8
 
@@ -133,13 +134,15 @@ export const getProductData = async ({
133
134
  groupProduct,
134
135
  headers
135
136
  }: GetProduct) => {
137
+ searchParams = normalizeSearchParams(searchParams);
138
+
136
139
  // Convert pk to number for cache key if it's a string
137
140
  const numericPkForCache = typeof pk === 'string' ? parseInt(pk, 10) : pk;
138
141
 
139
142
  const result = await Cache.wrap(
140
143
  CacheKey[groupProduct ? 'GroupProduct' : 'Product'](
141
144
  numericPkForCache,
142
- searchParams ?? new URLSearchParams()
145
+ searchParams ?? {}
143
146
  ),
144
147
  locale,
145
148
  getProductDataHandler({
@@ -2,6 +2,7 @@ import { Cache, CacheKey } from '../../lib/cache';
2
2
  import { category } from '../urls';
3
3
  import { GetCategoryResponse, SearchParams } from '../../types';
4
4
  import { generateCommerceSearchParams } from '../../utils';
5
+ import { normalizeSearchParams } from '../../utils/normalize-search-params';
5
6
  import appFetch from '../../utils/app-fetch';
6
7
  import { ServerVariables } from '../../utils/server-variables';
7
8
 
@@ -45,6 +46,8 @@ export const getSpecialPageData = async ({
45
46
  searchParams: SearchParams;
46
47
  headers?: Record<string, string>;
47
48
  }) => {
49
+ searchParams = normalizeSearchParams(searchParams);
50
+
48
51
  return Cache.wrap(
49
52
  CacheKey.SpecialPage(pk, searchParams, headers),
50
53
  locale,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@akinon/next",
3
3
  "description": "Core package for Project Zero Next",
4
- "version": "2.0.23-beta.0",
4
+ "version": "2.0.23",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "set-cookie-parser": "2.6.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@akinon/eslint-plugin-projectzero": "2.0.23-beta.0",
39
+ "@akinon/eslint-plugin-projectzero": "2.0.23",
40
40
  "@babel/core": "7.26.10",
41
41
  "@babel/preset-env": "7.26.9",
42
42
  "@babel/preset-typescript": "7.27.0",
package/utils/index.ts CHANGED
@@ -6,6 +6,7 @@ import getRootHostname from './get-root-hostname';
6
6
  export * from './get-currency';
7
7
  export * from './menu-generator';
8
8
  export * from './generate-commerce-search-params';
9
+ export * from './normalize-search-params';
9
10
  export * from './get-currency-label';
10
11
  export * from './pz-segments';
11
12
  export * from './get-checkout-path';
@@ -0,0 +1,42 @@
1
+ import { SearchParams } from '../types';
2
+
3
+ /**
4
+ * Normalize SearchParams to a plain Record<string, string | string[]>.
5
+ *
6
+ * Accepts either a URLSearchParams instance (Next 16 HOC output / v2 brands)
7
+ * or a plain Record (v1 brands / direct callers) and always returns a plain
8
+ * object. Downstream code can then safely:
9
+ * - JSON.stringify it for stable cache keys (avoids the URLSearchParams
10
+ * `JSON.stringify === "{}"` cache collision)
11
+ * - iterate with Object.keys / Object.entries (avoids the dropped query
12
+ * param bug on the PDP outbound URL builder)
13
+ * - spread with { ...obj }
14
+ *
15
+ * Repeated keys in URLSearchParams (e.g. ?color=red&color=blue) collapse to
16
+ * a string[] value so multi-value filter semantics survive.
17
+ *
18
+ * Returns undefined when the input is undefined so downstream
19
+ * `if (searchParams)` guards keep working unchanged.
20
+ */
21
+ export const normalizeSearchParams = (
22
+ searchParams?: SearchParams
23
+ ): Record<string, string | string[]> | undefined => {
24
+ if (!searchParams) return undefined;
25
+
26
+ if (searchParams instanceof URLSearchParams) {
27
+ const out: Record<string, string | string[]> = {};
28
+ for (const key of new Set(searchParams.keys())) {
29
+ const all = searchParams.getAll(key);
30
+ out[key] = all.length > 1 ? all : all[0];
31
+ }
32
+ return out;
33
+ }
34
+
35
+ // Plain object — drop undefined values so cache key serialization is stable
36
+ // across calls that omit some keys.
37
+ const out: Record<string, string | string[]> = {};
38
+ for (const [key, value] of Object.entries(searchParams)) {
39
+ if (value !== undefined) out[key] = value;
40
+ }
41
+ return out;
42
+ };