@akinon/next 1.126.19-v1-rc.16 → 1.126.19

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,12 +1,12 @@
1
1
  # @akinon/next
2
2
 
3
- ## 1.126.19-v1-rc.16
3
+ ## 1.126.19
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - d4809417: ZERO-4399: add card rewards to pz-masterpass-rest
8
- - a34bb1b: ZERO-5168: Post the attribute-based shipping selection to the page name Commerce reports
9
- - 182a639b: ZERO-4483: Verify v1-rc maintenance pipeline (no-op patch to confirm v1-rc dist-tag flow)
7
+ - df46ca2: ZERO-5207: Add getCollectionWidgetData to the v1 server data layer, as on main
8
+
9
+ `getCollectionWidgetData` reads a collection widget (`/product_collection_widgets/<slug>/`) through the core's cache, under `widget_collection:<slug>`, the way `getWidgetData` reads a widget. `@akinon/pz-theme` already looks the function up on `@akinon/next/data/server` and uses it when it is there; on a core without it, pz-theme calls the endpoint itself, without the cache. Like `getWidgetSchemaData` on this line, it skips main's preview-copy lookup, because the theme editor writes no preview copies of collection widgets.
10
10
 
11
11
  ## 1.126.18
12
12
 
@@ -0,0 +1,126 @@
1
+ jest.mock('server-only', () => ({}), { virtual: true });
2
+
3
+ const appFetch = jest.fn();
4
+
5
+ jest.mock('../utils/app-fetch', () => ({
6
+ __esModule: true,
7
+ default: (...args: unknown[]) => appFetch(...args)
8
+ }));
9
+
10
+ jest.mock('../utils/server-variables', () => ({
11
+ ServerVariables: { locale: 'tr', currency: 'try' }
12
+ }));
13
+
14
+ import * as serverData from '../data/server';
15
+ import { getCollectionWidgetData, getWidgetData } from '../data/server/widget';
16
+ import { Cache, CacheKey } from '../lib/cache';
17
+
18
+ const SLUG = 'home-product-slider';
19
+
20
+ const COLLECTION_WIDGET = {
21
+ pk: 7,
22
+ name: SLUG,
23
+ attributes: {},
24
+ products: [{ pk: 1, name: 'Shirt', absolute_url: '/shirt/' }]
25
+ };
26
+
27
+ describe('getCollectionWidgetData', () => {
28
+ let wrap: jest.SpyInstance;
29
+
30
+ beforeEach(() => {
31
+ appFetch.mockReset().mockResolvedValue(COLLECTION_WIDGET);
32
+ wrap = jest
33
+ .spyOn(Cache, 'wrap')
34
+ .mockImplementation(async (_key, _locale, handler) => handler());
35
+ });
36
+
37
+ afterEach(() => {
38
+ wrap.mockRestore();
39
+ });
40
+
41
+ // @akinon/pz-theme reads it off this module object and calls the endpoint
42
+ // itself, uncached, when it is missing.
43
+ it('is exported from the server data layer', () => {
44
+ expect(
45
+ (serverData as unknown as Record<string, unknown>).getCollectionWidgetData
46
+ ).toBe(getCollectionWidgetData);
47
+ });
48
+
49
+ it('reads the collection widget endpoint for the request locale and currency', async () => {
50
+ await expect(getCollectionWidgetData({ slug: SLUG })).resolves.toEqual(
51
+ COLLECTION_WIDGET
52
+ );
53
+
54
+ expect(appFetch).toHaveBeenCalledTimes(1);
55
+ expect(appFetch).toHaveBeenCalledWith({
56
+ url: `/product_collection_widgets/${SLUG}/`,
57
+ locale: 'tr',
58
+ currency: 'try',
59
+ init: {}
60
+ });
61
+ });
62
+
63
+ it('caches compressed, under its own key, per locale', async () => {
64
+ await getCollectionWidgetData({ slug: SLUG });
65
+
66
+ expect(wrap).toHaveBeenCalledWith(
67
+ `widget_collection:${SLUG}`,
68
+ 'tr',
69
+ expect.any(Function),
70
+ { compressed: true }
71
+ );
72
+ });
73
+
74
+ it('never shares a cache entry with a regular widget of the same slug', () => {
75
+ expect(CacheKey.Widget(`collection:${SLUG}`)).not.toBe(
76
+ CacheKey.Widget(SLUG)
77
+ );
78
+ });
79
+
80
+ it('honours an explicit locale, currency and cache options', async () => {
81
+ await getCollectionWidgetData({
82
+ slug: SLUG,
83
+ locale: 'en',
84
+ currency: 'usd',
85
+ cacheOptions: { expire: 30 }
86
+ });
87
+
88
+ expect(wrap).toHaveBeenCalledWith(
89
+ `widget_collection:${SLUG}`,
90
+ 'en',
91
+ expect.any(Function),
92
+ { compressed: true, expire: 30 }
93
+ );
94
+ expect(appFetch).toHaveBeenCalledWith({
95
+ url: `/product_collection_widgets/${SLUG}/`,
96
+ locale: 'en',
97
+ currency: 'usd',
98
+ init: {}
99
+ });
100
+ });
101
+
102
+ it('serves a cached answer without calling the api', async () => {
103
+ wrap.mockResolvedValue(COLLECTION_WIDGET);
104
+
105
+ await expect(getCollectionWidgetData({ slug: SLUG })).resolves.toEqual(
106
+ COLLECTION_WIDGET
107
+ );
108
+ expect(appFetch).not.toHaveBeenCalled();
109
+ });
110
+
111
+ it('answers null for an empty slug without calling the api', async () => {
112
+ await expect(getCollectionWidgetData({ slug: '' })).resolves.toBeNull();
113
+ expect(appFetch).not.toHaveBeenCalled();
114
+ });
115
+
116
+ it('is cached the way the widget answer is', async () => {
117
+ await getWidgetData({ slug: SLUG });
118
+ await getCollectionWidgetData({ slug: SLUG });
119
+
120
+ const [widgetOptions, collectionOptions] = wrap.mock.calls.map(
121
+ (call) => call[3]
122
+ );
123
+
124
+ expect(widgetOptions).toEqual(collectionOptions);
125
+ });
126
+ });
@@ -265,6 +265,55 @@ export const getWidgetData = async <T>({
265
265
  );
266
266
  };
267
267
 
268
+ const getCollectionWidgetDataHandler =
269
+ (
270
+ slug: string,
271
+ locale: string,
272
+ currency: string,
273
+ next?: { revalidate: number }
274
+ ) =>
275
+ async () => {
276
+ if (!slug) {
277
+ return null;
278
+ }
279
+
280
+ return await appFetch({
281
+ url: widgets.getCollectionWidget(slug),
282
+ locale,
283
+ currency,
284
+ init: next ? { next } : {}
285
+ });
286
+ };
287
+
288
+ /**
289
+ * A collection-backed widget (`product_collection_widgets`): what a theme
290
+ * section bound to a product collection renders, with the collection's live
291
+ * rows. Cached like the widget answer. Like getWidgetSchemaData on this line,
292
+ * it skips main's preview-copy lookup: the theme editor writes no preview
293
+ * copies of collection widgets (see ZERO-5149).
294
+ */
295
+ export const getCollectionWidgetData = async <T>({
296
+ slug,
297
+ locale = ServerVariables.locale,
298
+ currency = ServerVariables.currency,
299
+ cacheOptions
300
+ }: {
301
+ slug: string;
302
+ locale?: string;
303
+ currency?: string;
304
+ cacheOptions?: CacheOptions;
305
+ }): Promise<WidgetResultType<T>> => {
306
+ return Cache.wrap(
307
+ CacheKey.Widget(`collection:${slug}`),
308
+ locale,
309
+ getCollectionWidgetDataHandler(slug, locale, currency),
310
+ {
311
+ compressed: true,
312
+ ...cacheOptions
313
+ }
314
+ );
315
+ };
316
+
268
317
  const getWidgetSchemaDataHandler =
269
318
  (
270
319
  widgetSlug: string,
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": "1.126.19-v1-rc.16",
4
+ "version": "1.126.19",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -35,7 +35,7 @@
35
35
  "set-cookie-parser": "2.6.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@akinon/eslint-plugin-projectzero": "1.126.19-v1-rc.16",
38
+ "@akinon/eslint-plugin-projectzero": "1.126.19",
39
39
  "@babel/core": "7.26.10",
40
40
  "@babel/preset-env": "7.26.9",
41
41
  "@babel/preset-typescript": "7.27.0",