@akinon/next 1.34.0 → 1.36.0-rc.0

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/api/client.ts +24 -8
  3. package/bin/pz-install-extensions.js +27 -0
  4. package/bin/pz-install-plugins.js +1 -7
  5. package/bin/pz-install-theme.js +1 -10
  6. package/bin/pz-pre-check-dist.js +21 -0
  7. package/bin/pz-prebuild.js +2 -0
  8. package/bin/pz-predev.js +2 -0
  9. package/components/input.tsx +20 -7
  10. package/components/link.tsx +17 -13
  11. package/components/price.tsx +4 -3
  12. package/data/client/api.ts +1 -2
  13. package/data/server/category.ts +2 -2
  14. package/data/server/list.ts +2 -2
  15. package/data/server/product.ts +2 -5
  16. package/data/server/special-page.ts +2 -2
  17. package/{.eslintrc.js → eslint.config.js} +1 -2
  18. package/hooks/use-pagination.ts +21 -14
  19. package/lib/cache-handler.mjs +33 -0
  20. package/lib/cache.ts +18 -6
  21. package/middlewares/complete-gpay.ts +19 -0
  22. package/middlewares/complete-masterpass.ts +19 -0
  23. package/middlewares/default.ts +23 -0
  24. package/middlewares/index.ts +1 -0
  25. package/middlewares/pretty-url.ts +6 -0
  26. package/middlewares/redirection-payment.ts +19 -1
  27. package/middlewares/three-d-redirection.ts +19 -0
  28. package/package.json +7 -4
  29. package/routes/pretty-url.tsx +194 -0
  30. package/types/commerce/category.ts +1 -0
  31. package/types/index.ts +3 -1
  32. package/utils/app-fetch.ts +1 -1
  33. package/utils/check-monorepo.js +15 -0
  34. package/utils/find-base-dir.js +13 -0
  35. package/utils/format-cookie-string.ts +27 -0
  36. package/utils/generate-commerce-search-params.ts +6 -2
  37. package/utils/hash-directory.js +18 -0
  38. package/utils/index.ts +6 -0
  39. package/with-pz-config.js +11 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @akinon/next
2
2
 
3
+ ## 1.36.0-rc.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a4c8d6a: ZERO-2663: Fix the image url for gif and svgs and return them without options
8
+ - 8e6e8cf: ZERO-2524: Removed cli from projectzero so that build works as expected
9
+ - c53ef7b: ZERO-2668: The Link component has been updated to improve the logic for handling href values. Previously, if the href was not a string or started with 'http', it would return the href as is. Now, if the href is not provided, it will default to '#' to prevent any potential errors. Additionally, if the href is a string and does not start with 'http', it will be formatted with the locale and pathname, based on the localeUrlStrategy and defaultLocaleValue. This ensures that the correct href is generated based on the localization settings.
10
+ - ebb63ce: ZERO-2525: Fix category facet removal bug and add close icon to active filters
11
+ - 7cebe87: ZERO-2524: Add check monorepo utility function
12
+ - 91265bb: ZERO-2551: Improve pretty url and caching performance
13
+ - bbe18b9: ZERO-2575: Fix build error
14
+ - 3420416: ZERO-2533: extend eslint config from @akinon/next
15
+ - beb499e: ZERO-2551: Add new tsconfig paths
16
+ - 40ad73e: ZERO-2504: add cookie filter to api client request
17
+ - 495d155: ZERO-2524: Zero-cli dist checks for changes and renders it to the terminal.
18
+ - f046f8e: ZERO-2575: update version for react-number-format
19
+ - 3e68768: ZERO-2578: Add osessionid check in middleware
20
+
21
+ ### Patch Changes
22
+
23
+ - 616690d: ZERO-2635: remove maxRetries from redux
24
+ - 59fb7c3: ZERO-2504: get set-cookie headers from commerce request
25
+
26
+ ## 1.35.0
27
+
28
+ ### Minor Changes
29
+
30
+ - d09b677: ZERO-2577: Fix pagination bug and update usePagination hook and ensure pagination controls rendering correctly
31
+ - 6d4aadb: ZERO-2476: Auto install recommendenent extension
32
+ - f0c23bc: ZERO-2135: add custom not found page
33
+ - f488ea8: ZERO-2505: findBaseDir function is united and move on to the utils
34
+ - 6b2972b: ZERO-2514: Fix handling of status 204 and return Commerce status code in client proxy API.
35
+
3
36
  ## 1.34.0
4
37
 
5
38
  ## 1.33.2
package/api/client.ts CHANGED
@@ -2,6 +2,8 @@ import { ClientRequestOptions } from '../types';
2
2
  import { NextResponse } from 'next/server';
3
3
  import settings from 'settings';
4
4
  import logger from '../utils/log';
5
+ import formatCookieString from '../utils/format-cookie-string';
6
+ import cookieParser from 'set-cookie-parser';
5
7
 
6
8
  interface RouteParams {
7
9
  params: {
@@ -113,6 +115,14 @@ async function proxyRequest(...args) {
113
115
 
114
116
  try {
115
117
  const request = await fetch(url, fetchOptions);
118
+
119
+ // Using NextResponse.json with status 204 will cause an error
120
+ if (request.status === 204) {
121
+ return new Response(null, {
122
+ status: 204
123
+ });
124
+ }
125
+
116
126
  let response = {} as any;
117
127
 
118
128
  try {
@@ -131,20 +141,26 @@ async function proxyRequest(...args) {
131
141
  );
132
142
  }
133
143
 
134
- const setCookieHeader = request.headers.get('set-cookie');
144
+ const setCookieHeaders = request.headers.getSetCookie();
145
+ const exceptCookieKeys = ['pz-locale', 'pz-currency'];
146
+
147
+ const isExcludedCookie = (name: string) => exceptCookieKeys.includes(name);
148
+
149
+ const filteredCookies = cookieParser
150
+ .parse(setCookieHeaders)
151
+ .filter((cookie) => !isExcludedCookie(cookie.name));
152
+
135
153
  const responseHeaders: any = {};
136
154
 
137
- if (setCookieHeader) {
138
- responseHeaders['set-cookie'] = setCookieHeader;
155
+ if (filteredCookies.length > 0) {
156
+ responseHeaders['set-cookie'] = filteredCookies
157
+ .map(formatCookieString)
158
+ .join(', ');
139
159
  }
140
160
 
141
- const statusCode = new RegExp(/^20./).test(request.status.toString())
142
- ? 200
143
- : request.status;
144
-
145
161
  return NextResponse.json(
146
162
  options.responseType === 'text' ? { result: response } : response,
147
- { status: statusCode, headers: responseHeaders }
163
+ { status: request.status, headers: responseHeaders }
148
164
  );
149
165
  } catch (error) {
150
166
  logger.error('Client proxy request failed', error);
@@ -0,0 +1,27 @@
1
+ const { execSync } = require('child_process');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ function findBaseDir() {
6
+ let currentDir = __dirname;
7
+ while (currentDir !== path.resolve(currentDir, '..')) {
8
+ if (fs.existsSync(path.join(currentDir, 'turbo.json'))) {
9
+ return currentDir;
10
+ }
11
+ currentDir = path.resolve(currentDir, '..');
12
+ }
13
+ return null;
14
+ }
15
+
16
+ const BASE_DIR = findBaseDir();
17
+
18
+ if (BASE_DIR) {
19
+ const extensions = ['bilal-akinon.pznext'];
20
+ extensions.forEach((extension) => {
21
+ try {
22
+ execSync(`code --install-extension ${extension}`, { stdio: 'inherit' });
23
+ } catch (error) {
24
+ console.error(`Error installing ${extension}:`);
25
+ }
26
+ });
27
+ }
@@ -1,13 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { execSync } = require('child_process');
4
-
5
- function findBaseDir() {
6
- const insideNodeModules = __dirname.includes('node_modules');
7
- return insideNodeModules
8
- ? process.cwd()
9
- : path.resolve(__dirname, '../../../apps/projectzeronext');
10
- }
4
+ const findBaseDir = require('../utils/find-base-dir');
11
5
 
12
6
  const BASE_DIR = findBaseDir();
13
7
  const getFullPath = (relativePath) => path.join(BASE_DIR, relativePath);
@@ -3,16 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const spawn = require('cross-spawn');
6
-
7
- function findBaseDir() {
8
- const insideNodeModules = __dirname.includes('node_modules');
9
-
10
- if (insideNodeModules) {
11
- return path.resolve(__dirname, '../../../../');
12
- } else {
13
- return path.resolve(__dirname, '../../../apps/projectzeronext');
14
- }
15
- }
6
+ const findBaseDir = require('../utils/find-base-dir');
16
7
 
17
8
  const BASE_DIR = findBaseDir();
18
9
 
@@ -0,0 +1,21 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const hashDirectory = require('../utils/hash-directory');
4
+ const checkMonorepo = require('../utils/check-monorepo');
5
+
6
+ const BASE_DIR = checkMonorepo();
7
+
8
+ if (!BASE_DIR) {
9
+ process.exit(0);
10
+ }
11
+
12
+ const packageDistPath = path.join(
13
+ __dirname,
14
+ '../../../packages/projectzero/dist/commands'
15
+ );
16
+
17
+ const hash = hashDirectory(packageDistPath);
18
+ fs.writeFileSync(
19
+ path.join(__dirname, '../../../packages/projectzero/dist/dist-hash.txt'),
20
+ hash
21
+ );
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const runScript = require('./run-script');
4
+
4
5
  runScript('pz-install-theme.js');
6
+ runScript('pz-pre-check-dist.js');
package/bin/pz-predev.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const runScript = require('./run-script');
4
+
5
+ runScript('pz-install-extensions.js');
4
6
  runScript('pz-check-env.js');
5
7
  runScript('pz-install-theme.js');
@@ -1,17 +1,29 @@
1
1
  import clsx from 'clsx';
2
- import { forwardRef, FocusEvent, useState } from 'react';
2
+ import { forwardRef, FocusEvent, useState, Ref } from 'react';
3
3
  import { Controller } from 'react-hook-form';
4
- import NumberFormat, { NumberFormatProps } from 'react-number-format';
4
+ // @ts-ignore
5
+ import { PatternFormat, PatternFormatProps } from 'react-number-format';
5
6
  import { InputProps } from '../types';
6
7
  import { twMerge } from 'tailwind-merge';
7
8
 
9
+ const PatternFormatWithRef = forwardRef(
10
+ (props: PatternFormatProps, ref: Ref<HTMLInputElement>) => {
11
+ return <PatternFormat {...props} getInputRef={ref} />;
12
+ }
13
+ );
14
+ PatternFormatWithRef.displayName = 'PatternFormatWithRef';
15
+
8
16
  export const Input = forwardRef<
9
17
  HTMLInputElement,
10
18
  InputProps &
11
19
  Pick<
12
- NumberFormatProps,
13
- 'format' | 'mask' | 'allowEmptyFormatting' | 'onValueChange'
14
- >
20
+ PatternFormatProps,
21
+ 'mask' | 'allowEmptyFormatting' | 'onValueChange'
22
+ > & {
23
+ format?: string;
24
+ defaultValue?: string;
25
+ type?: string;
26
+ }
15
27
  >((props, ref) => {
16
28
  const [focused, setFocused] = useState(false);
17
29
  const [hasValue, setHasValue] = useState(false);
@@ -37,6 +49,7 @@ export const Input = forwardRef<
37
49
  ),
38
50
  props.className
39
51
  );
52
+
40
53
  const inputProps: any = {
41
54
  id,
42
55
  ref,
@@ -79,14 +92,14 @@ export const Input = forwardRef<
79
92
  <Controller
80
93
  name={props.name ?? ''}
81
94
  control={props.control}
82
- defaultValue={false}
83
95
  render={({ field }) => (
84
- <NumberFormat
96
+ <PatternFormatWithRef
85
97
  format={format}
86
98
  mask={mask ?? ''}
87
99
  {...rest}
88
100
  {...field}
89
101
  {...inputProps}
102
+ type={props.type as 'text' | 'password' | 'tel'}
90
103
  />
91
104
  )}
92
105
  />
@@ -10,28 +10,32 @@ type LinkProps = Omit<
10
10
  React.AnchorHTMLAttributes<HTMLAnchorElement>,
11
11
  keyof NextLinkProps
12
12
  > &
13
- NextLinkProps;
13
+ NextLinkProps & {
14
+ href: string;
15
+ };
14
16
 
15
17
  export const Link = ({ children, href, ...rest }: LinkProps) => {
16
18
  const { locale, defaultLocaleValue, localeUrlStrategy } = useLocalization();
17
19
  const formattedHref = useMemo(() => {
18
- if (typeof href !== 'string' || href.startsWith('http')) {
19
- return href;
20
+ if (!href) {
21
+ return '#';
20
22
  }
21
23
 
22
- const pathnameWithoutLocale = href.replace(urlLocaleMatcherRegex, '');
23
- const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
24
+ if (typeof href === 'string' && !href.startsWith('http')) {
25
+ const pathnameWithoutLocale = href.replace(urlLocaleMatcherRegex, '');
26
+ const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
24
27
 
25
- if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
26
- return hrefWithLocale;
27
- } else if (
28
- localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
29
- locale !== defaultLocaleValue
30
- ) {
31
- return hrefWithLocale;
28
+ if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
29
+ return hrefWithLocale;
30
+ } else if (
31
+ localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
32
+ locale !== defaultLocaleValue
33
+ ) {
34
+ return hrefWithLocale;
35
+ }
32
36
  }
33
37
 
34
- return href || '#';
38
+ return href;
35
39
  }, [href, defaultLocaleValue, locale, localeUrlStrategy]);
36
40
 
37
41
  return (
@@ -1,11 +1,12 @@
1
1
  import { useMemo } from 'react';
2
- import NumberFormat, { NumberFormatProps } from 'react-number-format';
2
+ // @ts-ignore
3
+ import { NumericFormat, NumericFormatProps } from 'react-number-format';
3
4
  import { getCurrency } from '@akinon/next/utils';
4
5
 
5
6
  import { useLocalization } from '@akinon/next/hooks';
6
7
  import { PriceProps } from '../types';
7
8
 
8
- export const Price = (props: NumberFormatProps & PriceProps) => {
9
+ export const Price = (props: NumericFormatProps & PriceProps) => {
9
10
  const {
10
11
  value,
11
12
  currencyCode,
@@ -39,7 +40,7 @@ export const Price = (props: NumberFormatProps & PriceProps) => {
39
40
  );
40
41
 
41
42
  return (
42
- <NumberFormat
43
+ <NumericFormat
43
44
  value={useNegative ? `-${useNegativeSpace}${_value}` : _value}
44
45
  {...{
45
46
  [useCurrencyAfterPrice ? 'suffix' : 'prefix']: currency
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  createApi,
3
3
  fetchBaseQuery,
4
- retry,
5
4
  BaseQueryFn,
6
5
  FetchBaseQueryError,
7
6
  FetchBaseQueryMeta,
@@ -65,7 +64,7 @@ const customBaseQuery: BaseQueryFn<
65
64
 
66
65
  export const api = createApi({
67
66
  reducerPath: 'api',
68
- baseQuery: retry(customBaseQuery, { maxRetries: 3 }),
67
+ baseQuery: customBaseQuery,
69
68
  tagTypes: [
70
69
  'Basket',
71
70
  'BasketB2b',
@@ -8,7 +8,7 @@ import logger from '../../utils/log';
8
8
 
9
9
  function getCategoryDataHandler(
10
10
  pk: number,
11
- searchParams?: URLSearchParams,
11
+ searchParams?: { [key: string]: string | string[] | undefined },
12
12
  headers?: Record<string, string>
13
13
  ) {
14
14
  return async function () {
@@ -78,7 +78,7 @@ export const getCategoryData = ({
78
78
  headers
79
79
  }: {
80
80
  pk: number;
81
- searchParams?: URLSearchParams;
81
+ searchParams?: { [key: string]: string | string[] | undefined };
82
82
  headers?: Record<string, string>;
83
83
  }) => {
84
84
  return Cache.wrap(
@@ -7,7 +7,7 @@ import { parse } from 'lossless-json';
7
7
  import logger from '../../utils/log';
8
8
 
9
9
  const getListDataHandler = (
10
- searchParams: URLSearchParams,
10
+ searchParams: { [key: string]: string | string[] | undefined },
11
11
  headers?: Record<string, string>
12
12
  ) => {
13
13
  return async function () {
@@ -54,7 +54,7 @@ export const getListData = async ({
54
54
  searchParams,
55
55
  headers
56
56
  }: {
57
- searchParams: URLSearchParams;
57
+ searchParams: { [key: string]: string | string[] | undefined };
58
58
  headers?: Record<string, string>;
59
59
  }) => {
60
60
  return Cache.wrap(
@@ -9,7 +9,7 @@ import appFetch from '../../utils/app-fetch';
9
9
 
10
10
  type GetProduct = {
11
11
  pk: number;
12
- searchParams?: URLSearchParams;
12
+ searchParams?: { [key: string]: string | string[] | undefined };
13
13
  groupProduct?: boolean;
14
14
  };
15
15
 
@@ -74,10 +74,7 @@ export const getProductData = async ({
74
74
  groupProduct
75
75
  }: GetProduct) => {
76
76
  return Cache.wrap(
77
- CacheKey[groupProduct ? 'GroupProduct' : 'Product'](
78
- pk,
79
- searchParams ?? new URLSearchParams()
80
- ),
77
+ CacheKey[groupProduct ? 'GroupProduct' : 'Product'](pk, searchParams ?? {}),
81
78
  getProductDataHandler({ pk, searchParams, groupProduct }),
82
79
  {
83
80
  expire: 300
@@ -7,7 +7,7 @@ import header from '../../redux/reducers/header';
7
7
 
8
8
  const getSpecialPageDataHandler = (
9
9
  pk: number,
10
- searchParams: URLSearchParams,
10
+ searchParams: { [key: string]: string | string[] | undefined },
11
11
  headers?: Record<string, string>
12
12
  ) => {
13
13
  return async function () {
@@ -34,7 +34,7 @@ export const getSpecialPageData = async ({
34
34
  headers
35
35
  }: {
36
36
  pk: number;
37
- searchParams: URLSearchParams;
37
+ searchParams: { [key: string]: string | string[] | undefined };
38
38
  headers?: Record<string, string>;
39
39
  }) => {
40
40
  return Cache.wrap(
@@ -5,7 +5,6 @@ module.exports = {
5
5
  es2021: true
6
6
  },
7
7
  extends: [
8
- 'eslint:recommended',
9
8
  'next/core-web-vitals',
10
9
  'eslint:recommended',
11
10
  'plugin:@typescript-eslint/recommended',
@@ -18,7 +17,7 @@ module.exports = {
18
17
  env: {
19
18
  node: true
20
19
  },
21
- files: ['.eslintrc.{js,cjs}', 'middlewares/default.ts'],
20
+ files: ['eslint.config.{js,cjs}', 'middlewares/default.ts'],
22
21
  rules: {
23
22
  '@akinon/projectzero/check-middleware-order': 'error'
24
23
  },
@@ -28,7 +28,11 @@ function reducer(state: InitialState, action: ACTIONTYPE) {
28
28
  case 'setPage':
29
29
  return { ...state, page: action.payload };
30
30
  case 'setLimit':
31
- return { ...state, limit: action.payload };
31
+ return {
32
+ ...state,
33
+ limit: action.payload,
34
+ last: Math.ceil(state.total / action.payload)
35
+ };
32
36
  default:
33
37
  throw new Error();
34
38
  }
@@ -46,10 +50,11 @@ export default function usePagination(
46
50
  () => new URLSearchParams(searchParams.toString()),
47
51
  [searchParams]
48
52
  );
53
+
49
54
  const { page, limit } = useMemo(
50
55
  () => ({
51
56
  page: _page || Number(searchParams.get('page')) || 1,
52
- limit: _limit || Number(searchParams.get('limit'))
57
+ limit: _limit || Number(searchParams.get('limit')) || 12
53
58
  }),
54
59
  [searchParams, _page, _limit]
55
60
  );
@@ -60,6 +65,7 @@ export default function usePagination(
60
65
  last: _last || Math.ceil(_total / limit) || 1,
61
66
  total: _total
62
67
  };
68
+
63
69
  const [state, dispatch] = useReducer(reducer, initialState);
64
70
 
65
71
  useEffect(() => {
@@ -93,23 +99,24 @@ export default function usePagination(
93
99
  [dispatch]
94
100
  );
95
101
 
96
- const pageList = useMemo(() => {
97
- return Array.from({ length: state.last }, (_, i) => {
98
- urlSearchParams.set('page', (i + 1).toString());
99
-
100
- return {
101
- page: i + 1,
102
- url: `${pathname}?${urlSearchParams.toString()}`
103
- };
104
- });
105
- }, [state.last, pathname, urlSearchParams]);
102
+ const pageList = useMemo(
103
+ () =>
104
+ Array.from({ length: state.last }, (_, i) => {
105
+ urlSearchParams.set('page', (i + 1).toString());
106
+ return {
107
+ page: i + 1,
108
+ url: `${pathname}?${urlSearchParams.toString()}`
109
+ };
110
+ }),
111
+ [state.last, pathname, urlSearchParams]
112
+ );
106
113
 
107
114
  const prev = useMemo(() => {
108
115
  if (state.page > 1) {
109
116
  urlSearchParams.set('page', (Number(state.page) - 1).toString());
110
117
  return `${pathname}?${urlSearchParams.toString()}`;
111
118
  }
112
- return null;
119
+ return '#';
113
120
  }, [state.page, pathname, urlSearchParams]);
114
121
 
115
122
  const next = useMemo(() => {
@@ -117,7 +124,7 @@ export default function usePagination(
117
124
  urlSearchParams.set('page', (Number(state.page) + 1).toString());
118
125
  return `${pathname}?${urlSearchParams.toString()}`;
119
126
  }
120
- return null;
127
+ return '#';
121
128
  }, [state.page, state.last, pathname, urlSearchParams]);
122
129
 
123
130
  return {
@@ -0,0 +1,33 @@
1
+ import { CacheHandler } from '@neshca/cache-handler';
2
+ import createLruHandler from '@neshca/cache-handler/local-lru';
3
+ import createRedisHandler from '@neshca/cache-handler/redis-stack';
4
+ import { createClient } from 'redis';
5
+
6
+ CacheHandler.onCreation(async () => {
7
+ const redisUrl = `redis://${process.env.CACHE_HOST}:${
8
+ process.env.CACHE_PORT
9
+ }/${process.env.CACHE_BUCKET ?? '0'}`;
10
+
11
+ const client = createClient({
12
+ url: redisUrl
13
+ });
14
+
15
+ client.on('error', (error) => {
16
+ console.error('Redis client error', { redisUrl, error });
17
+ });
18
+
19
+ await client.connect();
20
+
21
+ const redisHandler = await createRedisHandler({
22
+ client,
23
+ timeoutMs: 5000
24
+ });
25
+
26
+ const localHandler = createLruHandler();
27
+
28
+ return {
29
+ handlers: [redisHandler, localHandler]
30
+ };
31
+ });
32
+
33
+ export default CacheHandler;
package/lib/cache.ts CHANGED
@@ -20,13 +20,16 @@ const hashCacheKey = (object?: Record<string, string>) => {
20
20
  return `_${encodeURIComponent(cacheKey)}`;
21
21
  };
22
22
  export const CacheKey = {
23
- List: (searchParams: URLSearchParams, headers?: Record<string, string>) =>
23
+ List: (
24
+ searchParams: { [key: string]: string | string[] | undefined },
25
+ headers?: Record<string, string>
26
+ ) =>
24
27
  `list_${encodeURIComponent(JSON.stringify(searchParams))}${hashCacheKey(
25
28
  headers
26
29
  )}`,
27
30
  Category: (
28
31
  pk: number,
29
- searchParams?: URLSearchParams,
32
+ searchParams?: { [key: string]: string | string[] | undefined },
30
33
  headers?: Record<string, string>
31
34
  ) =>
32
35
  `category_${pk}_${encodeURIComponent(
@@ -35,15 +38,20 @@ export const CacheKey = {
35
38
  CategorySlug: (slug: string) => `category_${slug}`,
36
39
  SpecialPage: (
37
40
  pk: number,
38
- searchParams: URLSearchParams,
41
+ searchParams: { [key: string]: string | string[] | undefined },
39
42
  headers?: Record<string, string>
40
43
  ) =>
41
44
  `special_page_${pk}_${encodeURIComponent(
42
45
  JSON.stringify(searchParams)
43
46
  )}${hashCacheKey(headers)}`,
44
- Product: (pk: number, searchParams: URLSearchParams) =>
45
- `product_${pk}_${encodeURIComponent(JSON.stringify(searchParams))}`,
46
- GroupProduct: (pk: number, searchParams: URLSearchParams) =>
47
+ Product: (
48
+ pk: number,
49
+ searchParams: { [key: string]: string | string[] | undefined }
50
+ ) => `product_${pk}_${encodeURIComponent(JSON.stringify(searchParams))}`,
51
+ GroupProduct: (
52
+ pk: number,
53
+ searchParams: { [key: string]: string | string[] | undefined }
54
+ ) =>
47
55
  `group_product_${pk}_${encodeURIComponent(JSON.stringify(searchParams))}`,
48
56
  FlatPage: (pk: number) => `flat_page_${pk}`,
49
57
  LandingPage: (pk: number) => `landing_page_${pk}`,
@@ -158,6 +166,10 @@ export class Cache {
158
166
  handler: () => Promise<T>,
159
167
  options?: CacheOptions
160
168
  ): Promise<T> {
169
+ if (Settings.usePrettyUrlRoute) {
170
+ return await handler();
171
+ }
172
+
161
173
  const requiredVariables = [
162
174
  process.env.CACHE_HOST,
163
175
  process.env.CACHE_PORT,
@@ -33,6 +33,7 @@ const withCompleteGpay =
33
33
  async (req: PzNextRequest, event: NextFetchEvent) => {
34
34
  const url = req.nextUrl.clone();
35
35
  const ip = req.headers.get('x-forwarded-for') ?? '';
36
+ const sessionId = req.cookies.get('osessionid');
36
37
 
37
38
  if (url.search.indexOf('GPayCompletePage') === -1) {
38
39
  return middleware(req, event);
@@ -50,6 +51,24 @@ const withCompleteGpay =
50
51
  try {
51
52
  const body = await streamToString(req.body);
52
53
 
54
+ if (!sessionId) {
55
+ logger.warn(
56
+ 'Make sure that the SESSION_COOKIE_SAMESITE environment variable is set to None in Commerce.',
57
+ {
58
+ middleware: 'complete-masterpass',
59
+ ip
60
+ }
61
+ );
62
+
63
+ return NextResponse.redirect(
64
+ `${url.origin}${getUrlPathWithLocale(
65
+ '/orders/checkout/',
66
+ req.cookies.get('pz-locale')?.value
67
+ )}`,
68
+ 303
69
+ );
70
+ }
71
+
53
72
  const request = await fetch(requestUrl, {
54
73
  method: 'POST',
55
74
  headers: requestHeaders,
@@ -33,6 +33,7 @@ const withCompleteMasterpass =
33
33
  async (req: PzNextRequest, event: NextFetchEvent) => {
34
34
  const url = req.nextUrl.clone();
35
35
  const ip = req.headers.get('x-forwarded-for') ?? '';
36
+ const sessionId = req.cookies.get('osessionid');
36
37
 
37
38
  if (url.search.indexOf('MasterpassCompletePage') === -1) {
38
39
  return middleware(req, event);
@@ -50,6 +51,24 @@ const withCompleteMasterpass =
50
51
  try {
51
52
  const body = await streamToString(req.body);
52
53
 
54
+ if (!sessionId) {
55
+ logger.warn(
56
+ 'Make sure that the SESSION_COOKIE_SAMESITE environment variable is set to None in Commerce.',
57
+ {
58
+ middleware: 'complete-masterpass',
59
+ ip
60
+ }
61
+ );
62
+
63
+ return NextResponse.redirect(
64
+ `${url.origin}${getUrlPathWithLocale(
65
+ '/orders/checkout/',
66
+ req.cookies.get('pz-locale')?.value
67
+ )}`,
68
+ 303
69
+ );
70
+ }
71
+
53
72
  const request = await fetch(requestUrl, {
54
73
  method: 'POST',
55
74
  headers: requestHeaders,
@@ -153,6 +153,7 @@ const withPzDefault =
153
153
 
154
154
  req.middlewareParams = {
155
155
  commerceUrl,
156
+ found: true,
156
157
  rewrites: {}
157
158
  };
158
159
 
@@ -186,6 +187,28 @@ const withPzDefault =
186
187
  locale.length ? `${locale}/` : ''
187
188
  }${currency}${prettyUrl ?? pathnameWithoutLocale}`;
188
189
 
190
+ if (
191
+ !req.middlewareParams.found &&
192
+ Settings.customNotFoundEnabled
193
+ ) {
194
+ let pathname = url.pathname
195
+ .replace(/\/+$/, '')
196
+ .split('/');
197
+ url.pathname = url.pathname.replace(
198
+ pathname.pop(),
199
+ 'pz-not-found'
200
+ );
201
+ }
202
+
203
+ if (
204
+ Settings.usePrettyUrlRoute &&
205
+ url.searchParams.toString().length > 0
206
+ ) {
207
+ url.pathname =
208
+ url.pathname +
209
+ `searchparams|${url.searchParams.toString()}`;
210
+ }
211
+
189
212
  Settings.rewrites.forEach((rewrite) => {
190
213
  url.pathname = url.pathname.replace(
191
214
  rewrite.source,
@@ -26,6 +26,7 @@ export {
26
26
  export interface PzNextRequest extends NextRequest {
27
27
  middlewareParams: {
28
28
  commerceUrl: string;
29
+ found: boolean;
29
30
  rewrites: {
30
31
  locale?: string;
31
32
  prettyUrl?: string;
@@ -56,6 +56,10 @@ const resolvePrettyUrl = async (pathname: string, ip: string | null) => {
56
56
  const withPrettyUrl =
57
57
  (middleware: NextMiddleware) =>
58
58
  async (req: PzNextRequest, event: NextFetchEvent) => {
59
+ if (Settings.usePrettyUrlRoute) {
60
+ return middleware(req, event);
61
+ }
62
+
59
63
  const url = req.nextUrl.clone();
60
64
  const matchedLanguagePrefix = url.pathname.match(
61
65
  urlLocaleMatcherRegex
@@ -98,6 +102,8 @@ const withPrettyUrl =
98
102
  return middleware(req, event);
99
103
  }
100
104
 
105
+ req.middlewareParams.found = false;
106
+
101
107
  return middleware(req, event);
102
108
  };
103
109
 
@@ -34,6 +34,7 @@ const withRedirectionPayment =
34
34
  const url = req.nextUrl.clone();
35
35
  const searchParams = new URLSearchParams(url.search);
36
36
  const ip = req.headers.get('x-forwarded-for') ?? '';
37
+ const sessionId = req.cookies.get('osessionid');
37
38
 
38
39
  if (searchParams.get('page') !== 'RedirectionPageCompletePage') {
39
40
  return middleware(req, event);
@@ -46,12 +47,29 @@ const withRedirectionPayment =
46
47
  Cookie: req.headers.get('cookie') ?? '',
47
48
  'x-currency': req.cookies.get('pz-currency')?.value ?? '',
48
49
  'x-forwarded-for': ip
49
-
50
50
  };
51
51
 
52
52
  try {
53
53
  const body = await streamToString(req.body);
54
54
 
55
+ if (!sessionId) {
56
+ logger.warn(
57
+ 'Make sure that the SESSION_COOKIE_SAMESITE environment variable is set to None in Commerce.',
58
+ {
59
+ middleware: 'redirection-payment',
60
+ ip
61
+ }
62
+ );
63
+
64
+ return NextResponse.redirect(
65
+ `${url.origin}${getUrlPathWithLocale(
66
+ '/orders/checkout/',
67
+ req.cookies.get('pz-locale')?.value
68
+ )}`,
69
+ 303
70
+ );
71
+ }
72
+
55
73
  const request = await fetch(requestUrl, {
56
74
  method: 'POST',
57
75
  headers: requestHeaders,
@@ -33,6 +33,7 @@ const withThreeDRedirection =
33
33
  async (req: PzNextRequest, event: NextFetchEvent) => {
34
34
  const url = req.nextUrl.clone();
35
35
  const ip = req.headers.get('x-forwarded-for') ?? '';
36
+ const sessionId = req.cookies.get('osessionid');
36
37
 
37
38
  if (url.search.indexOf('CreditCardThreeDSecurePage') === -1) {
38
39
  return middleware(req, event);
@@ -50,6 +51,24 @@ const withThreeDRedirection =
50
51
  try {
51
52
  const body = await streamToString(req.body);
52
53
 
54
+ if (!sessionId) {
55
+ logger.warn(
56
+ 'Make sure that the SESSION_COOKIE_SAMESITE environment variable is set to None in Commerce.',
57
+ {
58
+ middleware: 'three-d-redirection',
59
+ ip
60
+ }
61
+ );
62
+
63
+ return NextResponse.redirect(
64
+ `${url.origin}${getUrlPathWithLocale(
65
+ '/orders/checkout/',
66
+ req.cookies.get('pz-locale')?.value
67
+ )}`,
68
+ 303
69
+ );
70
+ }
71
+
53
72
  const request = await fetch(requestUrl, {
54
73
  method: 'POST',
55
74
  headers: requestHeaders,
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.34.0",
4
+ "version": "1.36.0-rc.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -20,19 +20,22 @@
20
20
  "@opentelemetry/sdk-trace-node": "1.19.0",
21
21
  "@opentelemetry/semantic-conventions": "1.19.0",
22
22
  "@reduxjs/toolkit": "1.9.7",
23
+ "@neshca/cache-handler": "1.0.7",
23
24
  "cross-spawn": "7.0.3",
24
25
  "generic-pool": "3.9.0",
25
26
  "react-redux": "8.1.3",
26
27
  "react-string-replace": "1.1.1",
27
- "redis": "4.5.1",
28
- "semver": "7.5.4"
28
+ "redis": "4.6.13",
29
+ "semver": "7.5.4",
30
+ "set-cookie-parser": "2.6.0"
29
31
  },
30
32
  "devDependencies": {
31
33
  "@types/react-redux": "7.1.30",
34
+ "@types/set-cookie-parser": "2.4.7",
32
35
  "@typescript-eslint/eslint-plugin": "6.7.4",
33
36
  "@typescript-eslint/parser": "6.7.4",
34
37
  "eslint": "^8.14.0",
35
- "@akinon/eslint-plugin-projectzero": "1.34.0",
38
+ "@akinon/eslint-plugin-projectzero": "1.36.0-rc.0",
36
39
  "eslint-config-prettier": "8.5.0"
37
40
  }
38
41
  }
@@ -0,0 +1,194 @@
1
+ import { URLS } from '@akinon/next/data/urls';
2
+ import { Metadata, PageProps } from '@akinon/next/types';
3
+ import logger from '@akinon/next/utils/log';
4
+ import { notFound } from 'next/navigation';
5
+
6
+ type PrettyUrlResult = {
7
+ matched: boolean;
8
+ path?: string;
9
+ pk?: number;
10
+ };
11
+
12
+ const resolvePrettyUrlHandler =
13
+ (pathname: string, ip: string | null) => async () => {
14
+ let results = [] as { old_path: string }[];
15
+ let prettyUrlResult: PrettyUrlResult = {
16
+ matched: false
17
+ };
18
+
19
+ try {
20
+ const requestUrl = URLS.misc.prettyUrls(`/${pathname}/`);
21
+
22
+ logger.debug(`Resolving pretty url`, { pathname, requestUrl, ip });
23
+
24
+ const start = Date.now();
25
+ const apiResponse = await fetch(requestUrl, {
26
+ next: {
27
+ revalidate: 0
28
+ }
29
+ });
30
+ const data = await apiResponse.json();
31
+ ({ results } = data);
32
+ const end = Date.now();
33
+ console.warn('Pretty url response time', end - start, requestUrl);
34
+
35
+ const matched = results.length > 0;
36
+ const [{ old_path: path } = { old_path: '' }] = results;
37
+ let pk;
38
+
39
+ if (matched) {
40
+ const pkRegex = /\/(\d+)\/$/;
41
+ const match = path.match(pkRegex);
42
+ pk = match ? parseInt(match[1]) : undefined;
43
+ }
44
+
45
+ prettyUrlResult = {
46
+ matched,
47
+ path,
48
+ pk
49
+ };
50
+
51
+ logger.trace('Pretty url result', { prettyUrlResult, ip });
52
+ } catch (error) {
53
+ logger.error('Error resolving pretty url', { error, pathname, ip });
54
+ }
55
+
56
+ return prettyUrlResult;
57
+ };
58
+
59
+ export async function generateMetadata({ params }: PageProps) {
60
+ let result: Metadata = {};
61
+ const { prettyurl } = params;
62
+ const pageSlug = prettyurl
63
+ .filter((x) => !x.startsWith('searchparams'))
64
+ .join('/');
65
+
66
+ const searchParams = Object.fromEntries(
67
+ new URLSearchParams(
68
+ decodeURIComponent(
69
+ prettyurl
70
+ .find((x) => x.startsWith('searchparams'))
71
+ ?.replace('searchparams%7C', '')
72
+ ) ?? {}
73
+ ).entries()
74
+ );
75
+
76
+ const prettyUrlResult = await resolvePrettyUrlHandler(pageSlug, null)();
77
+
78
+ if (!prettyUrlResult.matched) {
79
+ return notFound();
80
+ }
81
+
82
+ const commonProps = {
83
+ params: {
84
+ ...params,
85
+ pk: prettyUrlResult.pk
86
+ },
87
+ searchParams
88
+ };
89
+
90
+ try {
91
+ if (prettyUrlResult.path.startsWith('/product/')) {
92
+ await import('@product/[pk]/page').then(async (module) => {
93
+ result = await module['generateMetadata']?.(commonProps);
94
+ });
95
+ }
96
+
97
+ if (prettyUrlResult.path.startsWith('/group-product/')) {
98
+ await import('@group-product/[pk]/page').then(async (module) => {
99
+ result = await module['generateMetadata']?.(commonProps);
100
+ });
101
+ }
102
+
103
+ if (prettyUrlResult.path.startsWith('/category/')) {
104
+ await import('@category/[pk]/page').then(async (module) => {
105
+ result = await module['generateMetadata']?.(commonProps);
106
+ });
107
+ }
108
+
109
+ if (prettyUrlResult.path.startsWith('/special-page/')) {
110
+ await import('@special-page/[pk]/page').then(async (module) => {
111
+ result = await module['generateMetadata']?.(commonProps);
112
+ });
113
+ }
114
+
115
+ if (prettyUrlResult.path.startsWith('/flat-page/')) {
116
+ await import('@flat-page/[pk]/page').then(async (module) => {
117
+ result = await module['generateMetadata']?.(commonProps);
118
+ });
119
+ }
120
+ // eslint-disable-next-line no-empty
121
+ } catch (error) {}
122
+
123
+ return result;
124
+ }
125
+
126
+ export const dynamic = 'force-static';
127
+ export const revalidate = 300;
128
+
129
+ export default async function Page({ params }) {
130
+ const { prettyurl } = params;
131
+ const pageSlug = prettyurl
132
+ .filter((x) => !x.startsWith('searchparams'))
133
+ .join('/');
134
+
135
+ const urlSearchParams = new URLSearchParams(
136
+ decodeURIComponent(
137
+ prettyurl
138
+ .find((x) => x.startsWith('searchparams'))
139
+ ?.replace('searchparams%7C', '')
140
+ ) ?? {}
141
+ );
142
+
143
+ const searchParams = {};
144
+
145
+ for (const [key, value] of urlSearchParams.entries() as unknown as Array<
146
+ [string, string]
147
+ >) {
148
+ if (!searchParams[key]) {
149
+ searchParams[key] = [];
150
+ }
151
+ searchParams[key].push(value);
152
+ }
153
+
154
+ const result = await resolvePrettyUrlHandler(pageSlug, null)();
155
+
156
+ if (!result.matched) {
157
+ return notFound();
158
+ }
159
+
160
+ const commonProps = {
161
+ params: {
162
+ ...params,
163
+ pk: result.pk
164
+ },
165
+ searchParams
166
+ };
167
+
168
+ if (result.path.startsWith('/category/')) {
169
+ const CategoryPage = (await import('@category/[pk]/page')).default;
170
+ return <CategoryPage {...commonProps} />;
171
+ }
172
+
173
+ if (result.path.startsWith('/product/')) {
174
+ const ProductPage = (await import('@product/[pk]/page')).default;
175
+ return <ProductPage {...commonProps} />;
176
+ }
177
+
178
+ if (result.path.startsWith('/group-product/')) {
179
+ const GroupProduct = (await import('@group-product/[pk]/page')).default;
180
+ return <GroupProduct {...commonProps} />;
181
+ }
182
+
183
+ if (result.path.startsWith('/special-page/')) {
184
+ const SpecialPage = (await import('@special-page/[pk]/page')).default;
185
+ return <SpecialPage {...commonProps} />;
186
+ }
187
+
188
+ if (result.path.startsWith('/flat-page/')) {
189
+ const FlatPage = (await import('@flat-page/[pk]/page')).default;
190
+ return <FlatPage {...commonProps} />;
191
+ }
192
+
193
+ return null;
194
+ }
@@ -36,6 +36,7 @@ export type FacetChoice = {
36
36
  quantity: number;
37
37
  is_selected: boolean;
38
38
  url?: string;
39
+ real_depth?: string;
39
40
  [key: string]: any;
40
41
  };
41
42
 
package/types/index.ts CHANGED
@@ -70,6 +70,7 @@ export interface Currency {
70
70
  }
71
71
 
72
72
  export interface Settings {
73
+ usePrettyUrlRoute?: boolean;
73
74
  commerceUrl: string;
74
75
  redis: {
75
76
  defaultExpirationTime: number;
@@ -181,6 +182,7 @@ export interface Settings {
181
182
  extraPaymentTypes?: string[];
182
183
  masterpassJsUrl?: string;
183
184
  };
185
+ customNotFoundEnabled: boolean;
184
186
  }
185
187
 
186
188
  export interface CacheOptions {
@@ -213,7 +215,7 @@ export type Translations = { [key: string]: object };
213
215
 
214
216
  export interface PageProps<T = any> {
215
217
  params: T;
216
- searchParams: URLSearchParams;
218
+ searchParams: { [key: string]: string | string[] | undefined };
217
219
  }
218
220
 
219
221
  export interface LayoutProps<T = any> extends PageProps<T> {
@@ -41,7 +41,7 @@ const appFetch = async <T>(
41
41
  };
42
42
 
43
43
  init.next = {
44
- revalidate: 60
44
+ revalidate: Settings.usePrettyUrlRoute ? 0 : 60
45
45
  };
46
46
 
47
47
  logger.debug(`FETCH START ${url}`, { requestURL, init, ip });
@@ -0,0 +1,15 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function checkMonorepo() {
5
+ let currentDir = __dirname;
6
+ while (currentDir !== path.resolve(currentDir, '..')) {
7
+ if (fs.existsSync(path.join(currentDir, 'turbo.json'))) {
8
+ return currentDir;
9
+ }
10
+ currentDir = path.resolve(currentDir, '..');
11
+ }
12
+ return null;
13
+ }
14
+
15
+ module.exports = checkMonorepo;
@@ -0,0 +1,13 @@
1
+ const path = require('path');
2
+
3
+ function findBaseDir() {
4
+ const insideNodeModules = __dirname.includes('node_modules');
5
+
6
+ if (insideNodeModules) {
7
+ return path.resolve(__dirname, '../../../../');
8
+ } else {
9
+ return path.resolve(__dirname, '../../../apps/projectzeronext');
10
+ }
11
+ }
12
+
13
+ module.exports = findBaseDir;
@@ -0,0 +1,27 @@
1
+ export default function formatCookieString(cookieObj: Record<string, any>) {
2
+ const cookieParts = [`${cookieObj.name}=${cookieObj.value}`];
3
+
4
+ if (cookieObj.expires) {
5
+ cookieParts.push(`Expires=${cookieObj.expires.toUTCString()}`);
6
+ }
7
+ if (cookieObj.maxAge) {
8
+ cookieParts.push(`Max-Age=${cookieObj.maxAge}`);
9
+ }
10
+ if (cookieObj.path) {
11
+ cookieParts.push(`Path=${cookieObj.path}`);
12
+ }
13
+ if (cookieObj.domain) {
14
+ cookieParts.push(`Domain=${cookieObj.domain}`);
15
+ }
16
+ if (cookieObj.secure) {
17
+ cookieParts.push('Secure');
18
+ }
19
+ if (cookieObj.httpOnly) {
20
+ cookieParts.push('HttpOnly');
21
+ }
22
+ if (cookieObj.sameSite) {
23
+ cookieParts.push(`SameSite=${cookieObj.sameSite}`);
24
+ }
25
+
26
+ return cookieParts.join('; ');
27
+ }
@@ -1,11 +1,15 @@
1
1
  import logger from './log';
2
2
 
3
- export const generateCommerceSearchParams = (searchParams?: URLSearchParams) => {
3
+ export const generateCommerceSearchParams = (searchParams?: {
4
+ [key: string]: string | string[] | undefined;
5
+ }) => {
4
6
  if (!searchParams) {
5
7
  return null;
6
8
  }
7
9
 
8
- const urlSerchParams = new URLSearchParams(searchParams);
10
+ const urlSerchParams = new URLSearchParams(
11
+ searchParams as Record<string, string>
12
+ );
9
13
 
10
14
  Object.entries(searchParams).forEach(([key, value]) => {
11
15
  if (typeof value === 'object') {
@@ -0,0 +1,18 @@
1
+ const { createHash } = require('crypto');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ function hashDirectory(directory) {
6
+ const files = fs.readdirSync(directory).sort();
7
+ const hash = createHash('sha256');
8
+ files.forEach((file) => {
9
+ const filePath = path.join(directory, file);
10
+ if (fs.statSync(filePath).isFile()) {
11
+ const fileBuffer = fs.readFileSync(filePath);
12
+ hash.update(fileBuffer);
13
+ }
14
+ });
15
+ return hash.digest('hex');
16
+ }
17
+
18
+ module.exports = hashDirectory;
package/utils/index.ts CHANGED
@@ -102,6 +102,12 @@ export function buildCDNUrl(url: string, config?: CDNOptions) {
102
102
  ''
103
103
  );
104
104
 
105
+ const noOptionFileExtension = url?.split('.').pop()?.toLowerCase() ?? '';
106
+
107
+ if (noOptionFileExtension === 'svg' || noOptionFileExtension === 'gif') {
108
+ return url;
109
+ }
110
+
105
111
  let options = '';
106
112
 
107
113
  if (config) {
package/with-pz-config.js CHANGED
@@ -8,6 +8,7 @@ const defaultConfig = {
8
8
  transpilePackages: ['@akinon/next', ...pzPlugins.map((p) => `@akinon/${p}`)],
9
9
  skipTrailingSlashRedirect: true,
10
10
  poweredByHeader: false,
11
+ cacheMaxMemorySize: 0,
11
12
  env: {
12
13
  NEXT_PUBLIC_SENTRY_DSN: process.env.SENTRY_DSN
13
14
  },
@@ -65,7 +66,12 @@ const defaultConfig = {
65
66
  }
66
67
  };
67
68
 
68
- const withPzConfig = (myNextConfig = {}) => {
69
+ const withPzConfig = (
70
+ myNextConfig = {},
71
+ options = {
72
+ useCacheHandler: false
73
+ }
74
+ ) => {
69
75
  let extendedConfig = deepMerge({}, defaultConfig, myNextConfig);
70
76
 
71
77
  const originalPzHeadersFunction = defaultConfig.headers;
@@ -91,6 +97,10 @@ const withPzConfig = (myNextConfig = {}) => {
91
97
  return [...pzRewrites, ...myRewrites];
92
98
  };
93
99
 
100
+ if (options.useCacheHandler) {
101
+ extendedConfig.cacheHandler = require.resolve('./lib/cache-handler.mjs');
102
+ }
103
+
94
104
  return extendedConfig;
95
105
  };
96
106