@websolutespa/bom-next 3.0.2 → 4.0.0-next.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.
Files changed (264) hide show
  1. package/CHANGELOG.md +37 -7
  2. package/README.md +28 -0
  3. package/dist/_virtual/{_rolldown/runtime.js → rolldown_runtime.js} +7 -4
  4. package/dist/client/client.d.ts.map +1 -1
  5. package/dist/client/client.js +17 -6
  6. package/dist/client/client.js.map +1 -1
  7. package/dist/client/extra.d.ts +3 -3
  8. package/dist/client/extra.d.ts.map +1 -1
  9. package/dist/client/extra.js +10 -5
  10. package/dist/client/extra.js.map +1 -1
  11. package/dist/client/label.d.ts +3 -3
  12. package/dist/client/label.d.ts.map +1 -1
  13. package/dist/client/label.js +10 -5
  14. package/dist/client/label.js.map +1 -1
  15. package/dist/client/layout.d.ts +3 -3
  16. package/dist/client/layout.d.ts.map +1 -1
  17. package/dist/client/layout.js +10 -5
  18. package/dist/client/layout.js.map +1 -1
  19. package/dist/client/local.collection.d.ts.map +1 -1
  20. package/dist/client/local.collection.js +4 -2
  21. package/dist/client/local.collection.js.map +1 -1
  22. package/dist/client/metadata.d.ts +3 -3
  23. package/dist/client/metadata.d.ts.map +1 -1
  24. package/dist/client/metadata.js +48 -40
  25. package/dist/client/metadata.js.map +1 -1
  26. package/dist/client/page.d.ts +3 -3
  27. package/dist/client/page.d.ts.map +1 -1
  28. package/dist/client/page.js +10 -5
  29. package/dist/client/page.js.map +1 -1
  30. package/dist/client/useApi.d.ts +7 -7
  31. package/dist/client/useApi.d.ts.map +1 -1
  32. package/dist/client/useApi.js +2 -1
  33. package/dist/client/useApi.js.map +1 -1
  34. package/dist/client/useCollectionApi.d.ts +6 -6
  35. package/dist/client/useCollectionApi.d.ts.map +1 -1
  36. package/dist/client/useCollectionApi.js +4 -2
  37. package/dist/client/useCollectionApi.js.map +1 -1
  38. package/dist/client/useExtra.d.ts.map +1 -1
  39. package/dist/client/useExtra.js +5 -2
  40. package/dist/client/useExtra.js.map +1 -1
  41. package/dist/client/useHttp.d.ts.map +1 -1
  42. package/dist/client/useHttp.js +9 -6
  43. package/dist/client/useHttp.js.map +1 -1
  44. package/dist/client/useLabel.d.ts.map +1 -1
  45. package/dist/client/useLabel.js +5 -2
  46. package/dist/client/useLabel.js.map +1 -1
  47. package/dist/client/useLayout.d.ts.map +1 -1
  48. package/dist/client/useLayout.js +5 -2
  49. package/dist/client/useLayout.js.map +1 -1
  50. package/dist/client/useLink.d.ts.map +1 -1
  51. package/dist/client/useLink.js +7 -3
  52. package/dist/client/useLink.js.map +1 -1
  53. package/dist/client/useMetaData.d.ts.map +1 -1
  54. package/dist/client/useMetaData.js +5 -2
  55. package/dist/client/useMetaData.js.map +1 -1
  56. package/dist/client/usePage.d.ts.map +1 -1
  57. package/dist/client/usePage.js +5 -2
  58. package/dist/client/usePage.js.map +1 -1
  59. package/dist/client/useUser.d.ts.map +1 -1
  60. package/dist/client/useUser.js +5 -2
  61. package/dist/client/useUser.js.map +1 -1
  62. package/dist/client/user.d.ts.map +1 -1
  63. package/dist/client/user.js +17 -11
  64. package/dist/client/user.js.map +1 -1
  65. package/dist/exports/client.js +2 -1
  66. package/dist/exports/instrumentation.js +2 -1
  67. package/dist/exports/proxy.js +2 -1
  68. package/dist/exports/server.d.ts +5 -4
  69. package/dist/exports/server.js +7 -5
  70. package/dist/index.d.ts +3 -3
  71. package/dist/index.js +3 -2
  72. package/dist/instrumentation/client.d.ts.map +1 -1
  73. package/dist/instrumentation/client.js +2 -1
  74. package/dist/instrumentation/client.js.map +1 -1
  75. package/dist/instrumentation/sentry/capture.requestError.js +4 -1
  76. package/dist/instrumentation/sentry/capture.requestError.js.map +1 -1
  77. package/dist/instrumentation/sentry/capture.routerTransitionStart.js +2 -1
  78. package/dist/instrumentation/sentry/capture.routerTransitionStart.js.map +1 -1
  79. package/dist/instrumentation/sentry/config.client.js +10 -2
  80. package/dist/instrumentation/sentry/config.client.js.map +1 -0
  81. package/dist/instrumentation/sentry/config.edge.js +10 -2
  82. package/dist/instrumentation/sentry/config.edge.js.map +1 -0
  83. package/dist/instrumentation/sentry/config.server.js +10 -2
  84. package/dist/instrumentation/sentry/config.server.js.map +1 -0
  85. package/dist/instrumentation/server.d.ts.map +1 -1
  86. package/dist/instrumentation/server.js +8 -5
  87. package/dist/instrumentation/server.js.map +1 -1
  88. package/dist/instrumentation/types.d.ts.map +1 -1
  89. package/dist/lib/breadcrumb.d.ts.map +1 -1
  90. package/dist/lib/breadcrumb.js +6 -1
  91. package/dist/lib/breadcrumb.js.map +1 -1
  92. package/dist/lib/const.d.ts +14 -2
  93. package/dist/lib/const.d.ts.map +1 -1
  94. package/dist/lib/const.js +24 -2
  95. package/dist/lib/const.js.map +1 -1
  96. package/dist/lib/http.d.ts +9 -0
  97. package/dist/lib/http.d.ts.map +1 -1
  98. package/dist/lib/http.js +27 -4
  99. package/dist/lib/http.js.map +1 -1
  100. package/dist/lib/label.d.ts.map +1 -1
  101. package/dist/lib/label.js +4 -3
  102. package/dist/lib/label.js.map +1 -1
  103. package/dist/lib/layout.defaults.d.ts.map +1 -1
  104. package/dist/lib/layout.defaults.js +1 -1
  105. package/dist/lib/layout.defaults.js.map +1 -1
  106. package/dist/lib/link.d.ts.map +1 -1
  107. package/dist/lib/link.js +2 -1
  108. package/dist/lib/link.js.map +1 -1
  109. package/dist/lib/meta-data.d.ts.map +1 -1
  110. package/dist/lib/open-graph.d.ts.map +1 -1
  111. package/dist/lib/pagination.d.ts.map +1 -1
  112. package/dist/lib/pagination.js +4 -2
  113. package/dist/lib/pagination.js.map +1 -1
  114. package/dist/lib/route.d.ts.map +1 -1
  115. package/dist/lib/route.js +9 -3
  116. package/dist/lib/route.js.map +1 -1
  117. package/dist/lib/store.d.ts.map +1 -1
  118. package/dist/lib/store.js +31 -31
  119. package/dist/lib/store.js.map +1 -1
  120. package/dist/lib/structured-data.d.ts.map +1 -1
  121. package/dist/lib/twitter-card.d.ts.map +1 -1
  122. package/dist/lib/utils.d.ts.map +1 -1
  123. package/dist/lib/utils.js +3 -2
  124. package/dist/lib/utils.js.map +1 -1
  125. package/dist/mock.d.ts.map +1 -1
  126. package/dist/node_modules/@selderee/plugin-htmlparser2/lib/hp2-builder.js +3 -2
  127. package/dist/node_modules/@selderee/plugin-htmlparser2/lib/hp2-builder.js.map +1 -1
  128. package/dist/node_modules/deepmerge/dist/cjs.js +17 -9
  129. package/dist/node_modules/deepmerge/dist/cjs.js.map +1 -1
  130. package/dist/node_modules/dom-serializer/lib/esm/foreignNames.js +2 -2
  131. package/dist/node_modules/dom-serializer/lib/esm/foreignNames.js.map +1 -1
  132. package/dist/node_modules/dom-serializer/lib/esm/index.js +10 -15
  133. package/dist/node_modules/dom-serializer/lib/esm/index.js.map +1 -1
  134. package/dist/node_modules/domelementtype/lib/esm/index.js +12 -12
  135. package/dist/node_modules/domelementtype/lib/esm/index.js.map +1 -1
  136. package/dist/node_modules/domhandler/lib/esm/index.js +4 -3
  137. package/dist/node_modules/domhandler/lib/esm/index.js.map +1 -1
  138. package/dist/node_modules/domhandler/lib/esm/node.js +8 -7
  139. package/dist/node_modules/domhandler/lib/esm/node.js.map +1 -1
  140. package/dist/node_modules/entities/lib/esm/decode.js +33 -32
  141. package/dist/node_modules/entities/lib/esm/decode.js.map +1 -1
  142. package/dist/node_modules/entities/lib/esm/decode_codepoint.js +5 -5
  143. package/dist/node_modules/entities/lib/esm/decode_codepoint.js.map +1 -1
  144. package/dist/node_modules/entities/lib/esm/escape.js +14 -6
  145. package/dist/node_modules/entities/lib/esm/escape.js.map +1 -1
  146. package/dist/node_modules/entities/lib/esm/generated/decode-data-html.js +2 -2
  147. package/dist/node_modules/entities/lib/esm/generated/decode-data-html.js.map +1 -1
  148. package/dist/node_modules/entities/lib/esm/generated/decode-data-xml.js +2 -2
  149. package/dist/node_modules/entities/lib/esm/generated/decode-data-xml.js.map +1 -1
  150. package/dist/node_modules/html-to-text/lib/html-to-text.js +45 -33
  151. package/dist/node_modules/html-to-text/lib/html-to-text.js.map +1 -1
  152. package/dist/node_modules/html-to-text/node_modules/htmlparser2/lib/esm/Parser.js +25 -25
  153. package/dist/node_modules/html-to-text/node_modules/htmlparser2/lib/esm/Parser.js.map +1 -1
  154. package/dist/node_modules/html-to-text/node_modules/htmlparser2/lib/esm/Tokenizer.js +76 -73
  155. package/dist/node_modules/html-to-text/node_modules/htmlparser2/lib/esm/Tokenizer.js.map +1 -1
  156. package/dist/node_modules/html-to-text/node_modules/htmlparser2/lib/esm/index.js +4 -3
  157. package/dist/node_modules/html-to-text/node_modules/htmlparser2/lib/esm/index.js.map +1 -1
  158. package/dist/node_modules/leac/lib/leac.js +72 -72
  159. package/dist/node_modules/leac/lib/leac.js.map +1 -1
  160. package/dist/node_modules/parseley/lib/parseley.js +37 -33
  161. package/dist/node_modules/parseley/lib/parseley.js.map +1 -1
  162. package/dist/node_modules/peberminta/lib/core.js +4 -4
  163. package/dist/node_modules/peberminta/lib/core.js.map +1 -1
  164. package/dist/node_modules/selderee/lib/selderee.js +16 -10
  165. package/dist/node_modules/selderee/lib/selderee.js.map +1 -1
  166. package/dist/proxy/protected-route.d.ts.map +1 -1
  167. package/dist/proxy/protected-route.js +11 -6
  168. package/dist/proxy/protected-route.js.map +1 -1
  169. package/dist/proxy/proxy.d.ts +5 -4
  170. package/dist/proxy/proxy.d.ts.map +1 -1
  171. package/dist/proxy/proxy.js +26 -6
  172. package/dist/proxy/proxy.js.map +1 -1
  173. package/dist/proxy/route-auto-detection.d.ts.map +1 -1
  174. package/dist/proxy/route-auto-detection.js +26 -14
  175. package/dist/proxy/route-auto-detection.js.map +1 -1
  176. package/dist/proxy/route-proxy.d.ts.map +1 -1
  177. package/dist/proxy/route-proxy.js +22 -5
  178. package/dist/proxy/route-proxy.js.map +1 -1
  179. package/dist/server/context.d.ts +31 -15
  180. package/dist/server/context.d.ts.map +1 -1
  181. package/dist/server/context.js +156 -69
  182. package/dist/server/context.js.map +1 -1
  183. package/dist/server/handlers/auth.handler.d.ts.map +1 -1
  184. package/dist/server/handlers/auth.handler.js +4 -2
  185. package/dist/server/handlers/auth.handler.js.map +1 -1
  186. package/dist/server/handlers/captions.handler.d.ts.map +1 -1
  187. package/dist/server/handlers/captions.handler.js +4 -2
  188. package/dist/server/handlers/captions.handler.js.map +1 -1
  189. package/dist/server/handlers/collection.handler.d.ts.map +1 -1
  190. package/dist/server/handlers/collection.handler.js +2 -1
  191. package/dist/server/handlers/collection.handler.js.map +1 -1
  192. package/dist/server/handlers/error-response.js +6 -2
  193. package/dist/server/handlers/error-response.js.map +1 -1
  194. package/dist/server/handlers/index.handler.d.ts.map +1 -1
  195. package/dist/server/handlers/index.handler.js +2 -1
  196. package/dist/server/handlers/index.handler.js.map +1 -1
  197. package/dist/server/handlers/preview.handler.d.ts.map +1 -1
  198. package/dist/server/handlers/preview.handler.js +6 -3
  199. package/dist/server/handlers/preview.handler.js.map +1 -1
  200. package/dist/server/handlers/registry.handler.d.ts.map +1 -1
  201. package/dist/server/handlers/registry.handler.js +14 -9
  202. package/dist/server/handlers/registry.handler.js.map +1 -1
  203. package/dist/server/handlers/route.handler.d.ts.map +1 -1
  204. package/dist/server/handlers/route.handler.js +12 -3
  205. package/dist/server/handlers/route.handler.js.map +1 -1
  206. package/dist/server/handlers/sitemap.handler.d.ts.map +1 -1
  207. package/dist/server/handlers/sitemap.handler.js +12 -9
  208. package/dist/server/handlers/sitemap.handler.js.map +1 -1
  209. package/dist/server/handlers/static-params.handler.d.ts.map +1 -1
  210. package/dist/server/handlers/static-params.handler.js +13 -8
  211. package/dist/server/handlers/static-params.handler.js.map +1 -1
  212. package/dist/server/handlers/vars.handler.d.ts.map +1 -1
  213. package/dist/server/handlers/vars.handler.js +5 -4
  214. package/dist/server/handlers/vars.handler.js.map +1 -1
  215. package/dist/server/helpers.d.ts +50 -0
  216. package/dist/server/helpers.d.ts.map +1 -0
  217. package/dist/server/helpers.js +60 -0
  218. package/dist/server/helpers.js.map +1 -0
  219. package/dist/server/layout.js +69 -34
  220. package/dist/server/layout.js.map +1 -1
  221. package/dist/server/lazy.d.ts +1 -0
  222. package/dist/server/lazy.d.ts.map +1 -1
  223. package/dist/server/lazy.js +3 -2
  224. package/dist/server/lazy.js.map +1 -1
  225. package/dist/server/local-api.d.ts.map +1 -1
  226. package/dist/server/local-api.js +12 -6
  227. package/dist/server/local-api.js.map +1 -1
  228. package/dist/server/metadata.d.ts +8 -5
  229. package/dist/server/metadata.d.ts.map +1 -1
  230. package/dist/server/metadata.js +21 -7
  231. package/dist/server/metadata.js.map +1 -1
  232. package/dist/server/mixer-payload.d.ts +1 -1
  233. package/dist/server/mixer-payload.d.ts.map +1 -1
  234. package/dist/server/mixer.d.ts +48 -6
  235. package/dist/server/mixer.d.ts.map +1 -1
  236. package/dist/server/mixer.js +235 -68
  237. package/dist/server/mixer.js.map +1 -1
  238. package/dist/server/page.d.ts +21 -5
  239. package/dist/server/page.d.ts.map +1 -1
  240. package/dist/server/page.js +74 -15
  241. package/dist/server/page.js.map +1 -1
  242. package/dist/server/utils.d.ts.map +1 -1
  243. package/dist/server/utils.js +4 -2
  244. package/dist/server/utils.js.map +1 -1
  245. package/dist/server/utmz.d.ts.map +1 -1
  246. package/dist/server/utmz.js +1 -1
  247. package/dist/server/utmz.js.map +1 -1
  248. package/dist/types.d.ts +79 -8
  249. package/dist/types.d.ts.map +1 -1
  250. package/package.json +16 -15
  251. package/src/exports/server.ts +1 -0
  252. package/src/lib/const.ts +15 -0
  253. package/src/lib/http.ts +15 -4
  254. package/src/proxy/proxy.ts +11 -4
  255. package/src/server/context.test.ts +467 -0
  256. package/src/server/context.ts +237 -78
  257. package/src/server/context.type-test.ts +106 -0
  258. package/src/server/helpers.ts +58 -0
  259. package/src/server/layout.ts +45 -39
  260. package/src/server/lazy.ts +1 -1
  261. package/src/server/metadata.ts +15 -7
  262. package/src/server/mixer.ts +57 -9
  263. package/src/server/page.ts +62 -17
  264. package/src/types.ts +109 -7
@@ -1,13 +1,28 @@
1
- 'use server';
1
+ import 'server-only';
2
2
 
3
- import { headers as getHeaders } from 'next/headers';
3
+ import { getIsDevelopment, type ILayout } from '@websolutespa/bom-core';
4
+ import type { User } from '@websolutespa/payload-plugin-bowl';
4
5
  import type { CollectionSlug, SanitizedConfig } from 'payload';
5
6
  import { cache } from 'react';
7
+ import { DEFAULT_LAYOUT_DEPTH, DEFAULT_PAGE_DEPTH } from '../lib/const';
6
8
  import { getLabelFromLabels } from '../lib/label';
7
9
  import { getLink } from '../lib/link';
8
- import type { IPageContext, TypedPageContext } from '../types';
10
+ import type {
11
+ IPageContext,
12
+ LayoutContextOptions,
13
+ LayoutOption,
14
+ LayoutOptions,
15
+ MixerContext,
16
+ PageContextOptions,
17
+ PageSelect,
18
+ ReadOptions,
19
+ ReadonlyHeaders,
20
+ TypedPage,
21
+ TypedPageContext
22
+ } from '../types';
9
23
  import { getLayout } from './layout';
10
- import { getPayload } from './mixer';
24
+ import { getPayload, resolveDraft } from './mixer';
25
+ import type { MixerPayload } from './mixer-payload';
11
26
  import { getPage } from './page';
12
27
 
13
28
  /**
@@ -18,6 +33,104 @@ const PageContext = cache(() => {
18
33
  return new Map<string, any>();
19
34
  });
20
35
 
36
+ /**
37
+ * Per-request memo of page and layout loads, keyed by a serialized string.
38
+ * React `cache` compares object arguments by identity, so options objects
39
+ * built inline by each caller would never hit: the key is serialized instead.
40
+ */
41
+ const RequestMemo = cache(() => {
42
+ return new Map<string, Promise<unknown>>();
43
+ });
44
+
45
+ /** Payload instance + authenticated user, resolved once per request. */
46
+ const getRequestPayload = cache((config: Promise<SanitizedConfig>, headers: ReadonlyHeaders | undefined, draft: boolean) => {
47
+ return getPayload({ config, headers, draft });
48
+ });
49
+
50
+ function memoize<T>(key: string, factory: () => Promise<T>): Promise<T> {
51
+ const memo = RequestMemo();
52
+ let promise = memo.get(key) as Promise<T> | undefined;
53
+ if (!promise) {
54
+ promise = factory();
55
+ memo.set(key, promise);
56
+ promise.catch(() => memo.delete(key));
57
+ }
58
+ return promise;
59
+ }
60
+
61
+ /** JSON serialization with sorted keys, so equivalent options produce the same key. */
62
+ export function stableStringify(value: unknown): string {
63
+ return JSON.stringify(value, (_, item) => {
64
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
65
+ return Object.keys(item).sort().reduce((sorted, key) => {
66
+ sorted[key] = item[key];
67
+ return sorted;
68
+ }, {} as Record<string, unknown>);
69
+ }
70
+ return item;
71
+ });
72
+ }
73
+
74
+ function getUserKey(user: User | null | undefined): string {
75
+ if (user && 'id' in user && user.id !== undefined && user.id !== null) {
76
+ return `${(user as { collection?: string }).collection || 'user'}:${user.id}`;
77
+ }
78
+ return 'guest';
79
+ }
80
+
81
+ function isPromiseLike(value: unknown): value is Promise<SanitizedConfig> {
82
+ return !!value && typeof (value as PromiseLike<unknown>).then === 'function';
83
+ }
84
+
85
+ const warnedLegacySignatures = new Set<string>();
86
+
87
+ function warnLegacySignature(name: string, signature: string) {
88
+ if (getIsDevelopment() && !warnedLegacySignatures.has(name)) {
89
+ warnedLegacySignatures.add(name);
90
+ console.warn(`[bom-next] ${name}(..., config, headers) is deprecated. Use ${name}(${signature}, { config, headers }) instead.`);
91
+ }
92
+ }
93
+
94
+ function toPageOptions<TSlug extends CollectionSlug, TSelect extends PageSelect<TSlug>, TLayout extends LayoutOption>(
95
+ name: string,
96
+ optionsOrConfig: PageContextOptions<TSlug, TSelect, TLayout> | Promise<SanitizedConfig>,
97
+ headers?: ReadonlyHeaders
98
+ ): PageContextOptions<TSlug, TSelect, TLayout> {
99
+ if (isPromiseLike(optionsOrConfig)) {
100
+ warnLegacySignature(name, 'market, locale, slug, id');
101
+ return { config: optionsOrConfig, headers };
102
+ }
103
+ return optionsOrConfig;
104
+ }
105
+
106
+ function loadLayout(
107
+ market: string,
108
+ locale: string,
109
+ payload: MixerPayload,
110
+ user: User,
111
+ mixerContext: MixerContext,
112
+ readOptions: { depth?: number; draft: boolean }
113
+ ): Promise<ILayout> {
114
+ const read = { depth: readOptions.depth ?? DEFAULT_LAYOUT_DEPTH, draft: readOptions.draft };
115
+ const key = stableStringify(['layout', market, locale, getUserKey(user), read]);
116
+ return memoize(key, () => getLayout(market, locale, payload, user, mixerContext, read));
117
+ }
118
+
119
+ function toPageContext<TSlug extends CollectionSlug, TSelect extends PageSelect<TSlug>, TLayout extends LayoutOption>(
120
+ layout: ILayout | undefined,
121
+ page: TypedPage<TSlug, TSelect> | undefined,
122
+ user: User
123
+ ): TypedPageContext<TSlug, TSelect, TLayout> {
124
+ return {
125
+ layout,
126
+ // shallow copy: the memoized document is shared by every caller in the request
127
+ page: page ? { ...page } : undefined,
128
+ user,
129
+ label: getLabelFromLabels(layout?.labels ?? []),
130
+ link: (category: string) => layout ? getLink(category, layout) : undefined,
131
+ } as TypedPageContext<TSlug, TSelect, TLayout>;
132
+ }
133
+
21
134
  export async function getPageContext<TSlug extends CollectionSlug = CollectionSlug>(): Promise<TypedPageContext<TSlug>> {
22
135
  return {
23
136
  layout: PageContext().get('layout'),
@@ -28,7 +141,9 @@ export async function getPageContext<TSlug extends CollectionSlug = CollectionSl
28
141
  } as TypedPageContext<TSlug>;
29
142
  }
30
143
 
31
- export async function setPageContext<TSlug extends CollectionSlug>(data: TypedPageContext<TSlug>) {
144
+ export async function setPageContext<TSlug extends CollectionSlug, TSelect extends PageSelect<TSlug> = undefined, TLayout extends LayoutOption = LayoutOptions>(
145
+ data: Partial<TypedPageContext<TSlug, TSelect, TLayout>>
146
+ ) {
32
147
  const context = PageContext();
33
148
  for (const key in data) {
34
149
  context.set(key, data[key as keyof IPageContext]);
@@ -36,112 +151,156 @@ export async function setPageContext<TSlug extends CollectionSlug>(data: TypedPa
36
151
  }
37
152
 
38
153
  /**
39
- * Same as createPageContext but without draft mode — used for error/not-found pages
40
- * that should always show published content regardless of draft mode state.
154
+ * Initialises the full page context using Payload local API.
155
+ *
156
+ * Page and layout are fetched in parallel and memoized per request, so
157
+ * `getMetadata` and the page component share the same reads. The mixer
158
+ * context (routes + categories) is passed to every payload read so that
159
+ * payload-plugin-bowl can enrich responses, including populated documents.
160
+ *
161
+ * @example
162
+ * const { page, label, link } = await createPageContext(market, locale, 'post_detail', id, {
163
+ * config,
164
+ * depth: 2,
165
+ * select: { title: true, abstract: true, media: true, components: true },
166
+ * populate: { media: { url: true, alt: true, width: true, height: true, type: true } },
167
+ * });
41
168
  */
42
- export async function createNotFoundContext<TSlug extends CollectionSlug>(
169
+ export function createPageContext<TSlug extends CollectionSlug, TSelect extends PageSelect<TSlug> = undefined, TLayout extends LayoutOption = LayoutOptions>(
170
+ market: string,
171
+ locale: string,
172
+ slug: TSlug,
173
+ id: string,
174
+ options: PageContextOptions<TSlug, TSelect, TLayout>
175
+ ): Promise<TypedPageContext<TSlug, TSelect, TLayout>>;
176
+ /** @deprecated Use `createPageContext(market, locale, slug, id, { config, headers })`. */
177
+ export function createPageContext<TSlug extends CollectionSlug>(
43
178
  market: string,
44
179
  locale: string,
45
180
  slug: TSlug,
46
181
  id: string,
47
182
  config: Promise<SanitizedConfig>,
48
- headers?: Awaited<ReturnType<typeof getHeaders>>
49
- ): Promise<TypedPageContext<TSlug>> {
50
- const { payload, user, getContext } = await getPayload({ config, headers });
183
+ headers?: ReadonlyHeaders
184
+ ): Promise<TypedPageContext<TSlug>>;
185
+ export async function createPageContext<TSlug extends CollectionSlug, TSelect extends PageSelect<TSlug> = undefined, TLayout extends LayoutOption = LayoutOptions>(
186
+ market: string,
187
+ locale: string,
188
+ slug: TSlug,
189
+ id: string,
190
+ optionsOrConfig: PageContextOptions<TSlug, TSelect, TLayout> | Promise<SanitizedConfig>,
191
+ headers?: ReadonlyHeaders
192
+ ): Promise<TypedPageContext<TSlug, TSelect, TLayout>> {
193
+ const options = toPageOptions<TSlug, TSelect, TLayout>('createPageContext', optionsOrConfig, headers);
194
+
195
+ const draft = await resolveDraft(options.draft);
196
+ const { payload, user, getContext } = await getRequestPayload(options.config, options.headers, draft);
51
197
  const context = await getContext(market, locale);
52
198
 
53
- const layout = await getLayout(market, locale, payload, user, context);
54
- const page = await getPage(slug, id, market, locale, payload, user, context);
199
+ const pageRead: ReadOptions = {
200
+ draft,
201
+ depth: options.depth ?? DEFAULT_PAGE_DEPTH,
202
+ select: options.select as ReadOptions['select'],
203
+ populate: options.populate,
204
+ joins: options.joins as ReadOptions['joins'],
205
+ };
206
+ const pageKey = stableStringify(['page', market, locale, slug, id, getUserKey(user), pageRead]);
207
+ const pagePromise = memoize(pageKey, () =>
208
+ getPage<TSlug, TSelect>(slug, id, market, locale, payload, user, context, pageRead)
209
+ );
55
210
 
56
- const label = getLabelFromLabels(layout.labels);
57
- const link = (category: string) => getLink(category, layout);
211
+ const layoutOptions = options.layout;
212
+ const layoutPromise = layoutOptions === false
213
+ ? Promise.resolve(undefined)
214
+ : (async () => loadLayout(market, locale, payload, user, context, {
215
+ depth: layoutOptions?.depth,
216
+ draft: layoutOptions?.draft === undefined ? draft : await resolveDraft(layoutOptions.draft),
217
+ }))();
58
218
 
59
- const pageContext = {
60
- layout,
61
- page,
62
- user,
63
- label,
64
- link,
65
- } as TypedPageContext<TSlug>;
219
+ const [layout, page] = await Promise.all([layoutPromise, pagePromise]);
220
+
221
+ const pageContext = toPageContext<TSlug, TSelect, TLayout>(layout, page, user);
66
222
 
67
- setPageContext(pageContext);
223
+ if (layout) {
224
+ await setPageContext(pageContext);
225
+ } else {
226
+ // layout skipped: keep any layout already stored for this request
227
+ await setPageContext({ page: pageContext.page, user: pageContext.user });
228
+ }
68
229
 
69
230
  return pageContext;
70
231
  }
71
232
 
72
233
  /**
73
- * Initialises the full page context using Payload local API.
74
- *
75
- * Replaces the @websolutespa/bom-next/server createPageContext that goes through the store
76
- * abstraction. All data is fetched directly from Payload; the mixer context
77
- * (routes + categories) is passed to every payload.find call so that
78
- * payload-plugin-bowl can enrich responses.
234
+ * Same as createPageContext but without draft mode — used for error/not-found pages
235
+ * that should always show published content regardless of draft mode state.
79
236
  */
80
- export async function createPageContext<TSlug extends CollectionSlug>(
237
+ export function createNotFoundContext<TSlug extends CollectionSlug, TSelect extends PageSelect<TSlug> = undefined, TLayout extends LayoutOption = LayoutOptions>(
238
+ market: string,
239
+ locale: string,
240
+ slug: TSlug,
241
+ id: string,
242
+ options: PageContextOptions<TSlug, TSelect, TLayout>
243
+ ): Promise<TypedPageContext<TSlug, TSelect, TLayout>>;
244
+ /** @deprecated Use `createNotFoundContext(market, locale, slug, id, { config, headers })`. */
245
+ export function createNotFoundContext<TSlug extends CollectionSlug>(
81
246
  market: string,
82
247
  locale: string,
83
248
  slug: TSlug,
84
249
  id: string,
85
250
  config: Promise<SanitizedConfig>,
86
- headers?: Awaited<ReturnType<typeof getHeaders>>
87
- ): Promise<TypedPageContext<TSlug>> {
88
-
89
- // const draft = await draftMode();
90
- const draft = { isEnabled: false };
91
-
92
- const { payload, user, getContext } = await getPayload({ config, headers });
93
- const context = await getContext(market, locale);
94
-
95
- const layout = await getLayout(market, locale, payload, user, context, draft.isEnabled);
96
- const page = await getPage(slug, id, market, locale, payload, user, context, draft.isEnabled);
97
-
98
- const label = getLabelFromLabels(layout.labels);
99
- const link = (category: string) => getLink(category, layout);
100
-
101
- const pageContext = {
102
- layout,
103
- page,
104
- user,
105
- label,
106
- link,
107
- } as TypedPageContext<TSlug>;
108
-
109
- setPageContext(pageContext);
110
-
111
- /*
112
- if (draft.isEnabled) {
113
- draft.disable();
114
- }
115
- */
116
-
117
- return pageContext;
251
+ headers?: ReadonlyHeaders
252
+ ): Promise<TypedPageContext<TSlug>>;
253
+ export async function createNotFoundContext<TSlug extends CollectionSlug, TSelect extends PageSelect<TSlug> = undefined, TLayout extends LayoutOption = LayoutOptions>(
254
+ market: string,
255
+ locale: string,
256
+ slug: TSlug,
257
+ id: string,
258
+ optionsOrConfig: PageContextOptions<TSlug, TSelect, TLayout> | Promise<SanitizedConfig>,
259
+ headers?: ReadonlyHeaders
260
+ ): Promise<TypedPageContext<TSlug, TSelect, TLayout>> {
261
+ const options = toPageOptions<TSlug, TSelect, TLayout>('createNotFoundContext', optionsOrConfig, headers);
262
+ const layout = (options.layout === false ? false : { ...options.layout, draft: false }) as TLayout;
263
+ return createPageContext<TSlug, TSelect, TLayout>(market, locale, slug, id, { ...options, draft: false, layout });
118
264
  }
119
265
 
120
266
  /**
121
267
  * Layout-only context — no page fetch. Used by RootLayout server components.
122
268
  */
123
- export async function createLayoutContext(
269
+ export function createLayoutContext(
270
+ market: string,
271
+ locale: string,
272
+ options: LayoutContextOptions
273
+ ): Promise<IPageContext>;
274
+ /** @deprecated Use `createLayoutContext(market, locale, { config, headers })`. */
275
+ export function createLayoutContext(
124
276
  market: string,
125
277
  locale: string,
126
278
  config: Promise<SanitizedConfig>,
127
- headers?: Awaited<ReturnType<typeof getHeaders>>
128
- ) {
129
- const { payload, user, getContext } = await getPayload({ config, headers });
130
- const context = await getContext(market, locale);
279
+ headers?: ReadonlyHeaders
280
+ ): Promise<IPageContext>;
281
+ export async function createLayoutContext(
282
+ market: string,
283
+ locale: string,
284
+ optionsOrConfig: LayoutContextOptions | Promise<SanitizedConfig>,
285
+ headers?: ReadonlyHeaders
286
+ ): Promise<IPageContext> {
287
+ let options: LayoutContextOptions;
288
+ if (isPromiseLike(optionsOrConfig)) {
289
+ warnLegacySignature('createLayoutContext', 'market, locale');
290
+ options = { config: optionsOrConfig, headers };
291
+ } else {
292
+ options = optionsOrConfig;
293
+ }
131
294
 
132
- const layout = await getLayout(market, locale, payload, user, context);
295
+ const draft = await resolveDraft(options.draft);
296
+ const { payload, user, getContext } = await getRequestPayload(options.config, options.headers, draft);
297
+ const context = await getContext(market, locale);
133
298
 
134
- const label = getLabelFromLabels(layout.labels);
135
- const link = (category: string) => getLink(category, layout);
299
+ const layout = await loadLayout(market, locale, payload, user, context, { depth: options.depth, draft });
136
300
 
137
- const pageContext = {
138
- layout,
139
- user,
140
- label,
141
- link,
142
- } as unknown as IPageContext;
301
+ const { page: _page, ...pageContext } = toPageContext<CollectionSlug, undefined, LayoutOptions>(layout, undefined, user);
143
302
 
144
- setPageContext(pageContext);
303
+ await setPageContext(pageContext);
145
304
 
146
- return pageContext;
305
+ return pageContext as IPageContext;
147
306
  }
@@ -0,0 +1,106 @@
1
+ import type { SanitizedConfig } from 'payload';
2
+ import type { ILayout } from '@websolutespa/bom-core';
3
+ import type { LayoutOption, Populated, ReadonlyHeaders, TypedPageContext } from '../types';
4
+ import { createLayoutContext, createNotFoundContext, createPageContext } from './context';
5
+ import { createContextHelpers } from './helpers';
6
+
7
+ declare const config: Promise<SanitizedConfig>;
8
+
9
+ type Expect<T extends true> = T;
10
+ type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
11
+
12
+ // options form, full document
13
+ const full = createPageContext('ww', 'it', 'page', 'id', { config, depth: 1, draft: 'auto' });
14
+ full.then(({ page, layout, label, link }) => {
15
+ page?.title;
16
+ page?.href;
17
+ page?.breadcrumb;
18
+ layout.tree;
19
+ label('key');
20
+ link('category');
21
+ });
22
+
23
+ // select narrows the page document type
24
+ const narrowed = createPageContext('ww', 'it', 'page', 'id', {
25
+ config,
26
+ select: { title: true, slug: true },
27
+ populate: { media: { url: true } },
28
+ layout: false,
29
+ });
30
+ narrowed.then(({ page }) => {
31
+ page?.title;
32
+ // envelope fields are always selected
33
+ page?.category;
34
+ page?.template;
35
+ page?.href;
36
+ page?.breadcrumb;
37
+ // @ts-expect-error not selected
38
+ page?.order;
39
+ });
40
+
41
+ // exclude mode keeps the envelope fields
42
+ createPageContext('ww', 'it', 'page', 'id', { config, select: { title: false, order: false } }).then(({ page }) => {
43
+ page?.title;
44
+ page?.media;
45
+ // @ts-expect-error excluded
46
+ page?.order;
47
+ });
48
+
49
+ // legacy positional form still compiles
50
+ const legacy = createPageContext('ww', 'it', 'page', 'id', config);
51
+ legacy.then((context) => {
52
+ type _check = Expect<Equal<typeof context, TypedPageContext<'page'>>>;
53
+ });
54
+ createNotFoundContext('ww', 'it', 'page', 'id', config);
55
+ createNotFoundContext('ww', 'it', 'page', 'id', { config, depth: 0 });
56
+ createLayoutContext('ww', 'it', config);
57
+ createLayoutContext('ww', 'it', { config, depth: 1 });
58
+
59
+ // @ts-expect-error config is required
60
+ createPageContext('ww', 'it', 'page', 'id', { depth: 1 });
61
+
62
+ const helpers = createContextHelpers({ config, defaults: { depth: 2 } });
63
+ helpers.createPageContext('ww', 'it', 'page', 'id').then(({ page }) => page?.title);
64
+ helpers.createPageContext('ww', 'it', 'page', 'id', { select: { title: true } }).then(({ page }) => {
65
+ // @ts-expect-error not selected
66
+ page?.order;
67
+ });
68
+ helpers.createLayoutContext('ww', 'it');
69
+ helpers.getMetadata('page');
70
+
71
+ // Populated narrows ids out of relation unions
72
+ type _populated = Expect<Equal<Populated<string | { url: string }>, { url: string }>>;
73
+ type _populatedArray = Expect<Equal<Populated<(number | { url: string })[]>, { url: string }[]>>;
74
+
75
+ // layout follows the layout option
76
+ declare const layoutOption: LayoutOption;
77
+ createPageContext('ww', 'it', 'page', 'id', { config }).then(({ layout }) => {
78
+ type _check = Expect<Equal<typeof layout, ILayout>>;
79
+ });
80
+ createPageContext('ww', 'it', 'page', 'id', { config, layout: { depth: 0 } }).then(({ layout }) => {
81
+ type _check = Expect<Equal<typeof layout, ILayout>>;
82
+ });
83
+ createPageContext('ww', 'it', 'page', 'id', { config, layout: false }).then(({ layout }) => {
84
+ type _check = Expect<Equal<typeof layout, undefined>>;
85
+ });
86
+ createPageContext('ww', 'it', 'page', 'id', { config, layout: layoutOption }).then(({ layout }) => {
87
+ type _check = Expect<Equal<typeof layout, ILayout | undefined>>;
88
+ });
89
+ createNotFoundContext('ww', 'it', 'page', 'id', { config, layout: false }).then(({ layout }) => {
90
+ type _check = Expect<Equal<typeof layout, undefined>>;
91
+ });
92
+ helpers.createPageContext('ww', 'it', 'page', 'id', { layout: false }).then(({ layout }) => {
93
+ type _check = Expect<Equal<typeof layout, undefined>>;
94
+ });
95
+ helpers.createPageContext('ww', 'it', 'page', 'id').then(({ layout }) => {
96
+ type _check = Expect<Equal<typeof layout, ILayout>>;
97
+ });
98
+
99
+ // headers are request scoped: per call only, never a bound default
100
+ declare const headers: ReadonlyHeaders;
101
+ // @ts-expect-error headers is not a default
102
+ createContextHelpers({ config, defaults: { headers } });
103
+ // @ts-expect-error skipping the layout is a per-call choice
104
+ createContextHelpers({ config, defaults: { layout: false } });
105
+ helpers.createPageContext('ww', 'it', 'page', 'id', { headers });
106
+ helpers.createLayoutContext('ww', 'it', { headers });
@@ -0,0 +1,58 @@
1
+ import type { CollectionSlug, SanitizedConfig } from 'payload';
2
+ import type { LayoutContextOptions, LayoutOption, LayoutOptions, PageContextOptions, PageSelect } from '../types';
3
+ import { createLayoutContext, createNotFoundContext, createPageContext } from './context';
4
+ import { getMetadata } from './metadata';
5
+
6
+ /**
7
+ * Options bound once for every call. Request-scoped values (`headers`) and
8
+ * per-page values (`select`, `populate`, `joins`) are passed per call;
9
+ * skipping the layout (`layout: false`) is a per-call choice too.
10
+ */
11
+ type ContextDefaults = Omit<PageContextOptions, 'config' | 'headers' | 'select' | 'populate' | 'joins' | 'layout'> & {
12
+ layout?: LayoutOptions;
13
+ };
14
+
15
+ type PageOptions<TSlug extends CollectionSlug, TSelect extends PageSelect<TSlug>, TLayout extends LayoutOption> =
16
+ Omit<PageContextOptions<TSlug, TSelect, TLayout>, 'config'> & { config?: Promise<SanitizedConfig> };
17
+
18
+ /**
19
+ * Binds `config` and default options once, so templates don't pass them every time.
20
+ * Per-call options override the defaults.
21
+ *
22
+ * @example
23
+ * // src/lib/context.ts in the app
24
+ * export const { createPageContext, createLayoutContext, getMetadata } =
25
+ * createContextHelpers({ config, defaults: { depth: 2 } });
26
+ */
27
+ export function createContextHelpers({
28
+ config,
29
+ defaults = {},
30
+ }: {
31
+ config: Promise<SanitizedConfig>;
32
+ defaults?: ContextDefaults;
33
+ }) {
34
+ const layoutDefaults: Omit<LayoutContextOptions, 'config'> = {
35
+ draft: defaults.draft,
36
+ ...defaults.layout,
37
+ };
38
+ return {
39
+ createPageContext<TSlug extends CollectionSlug, TSelect extends PageSelect<TSlug> = undefined, TLayout extends LayoutOption = LayoutOptions>(
40
+ market: string, locale: string, slug: TSlug, id: string, options?: PageOptions<TSlug, TSelect, TLayout>
41
+ ) {
42
+ return createPageContext<TSlug, TSelect, TLayout>(market, locale, slug, id, { config, ...defaults as PageOptions<TSlug, TSelect, TLayout>, ...options });
43
+ },
44
+ createNotFoundContext<TSlug extends CollectionSlug, TSelect extends PageSelect<TSlug> = undefined, TLayout extends LayoutOption = LayoutOptions>(
45
+ market: string, locale: string, slug: TSlug, id: string, options?: PageOptions<TSlug, TSelect, TLayout>
46
+ ) {
47
+ return createNotFoundContext<TSlug, TSelect, TLayout>(market, locale, slug, id, { config, ...defaults as PageOptions<TSlug, TSelect, TLayout>, ...options });
48
+ },
49
+ createLayoutContext(market: string, locale: string, options?: Partial<LayoutContextOptions>) {
50
+ return createLayoutContext(market, locale, { config, ...layoutDefaults, ...options });
51
+ },
52
+ getMetadata<TSlug extends CollectionSlug, TSelect extends PageSelect<TSlug> = undefined>(
53
+ slug: TSlug, options?: Omit<PageContextOptions<TSlug, TSelect, LayoutOption>, 'config'>
54
+ ) {
55
+ return getMetadata<TSlug, TSelect>(slug, config, { ...defaults, ...options });
56
+ },
57
+ };
58
+ }
@@ -1,11 +1,12 @@
1
- 'use server';
1
+ import 'server-only';
2
2
 
3
3
  import { IApp, IAppExtended, ICategory, ILabel, ILayout, ILocale, ILocalizedMenu, IMarket, IRoute, IRouteLink, getRootCategory } from '@websolutespa/bom-core';
4
4
  import type { User } from '@websolutespa/payload-plugin-bowl';
5
5
  import type { CollectionSlug, GlobalSlug } from 'payload';
6
6
  import { getRouteLinkCategory } from '../lib/route';
7
- import type { MixerContext } from '../types';
8
- import { buildBaseOptions } from './mixer';
7
+ import { DEFAULT_LAYOUT_DEPTH } from '../lib/const';
8
+ import type { MixerContext, ReadOptions } from '../types';
9
+ import { buildReadOptions } from './mixer';
9
10
  import type { MixerPayload } from './mixer-payload';
10
11
 
11
12
  /**
@@ -23,6 +24,9 @@ import type { MixerPayload } from './mixer-payload';
23
24
  *
24
25
  * The route link tree is built from mixerContext.currentRoutes + mixerContext.categories
25
26
  * using getRouteLinkCategory (pure fn, no store calls) from bom-mixer-models/server.
27
+ *
28
+ * The last argument accepts read options (depth, draft); depth defaults to
29
+ * DEFAULT_LAYOUT_DEPTH. A boolean is still accepted as the legacy draft flag.
26
30
  */
27
31
  export async function getLayout<T extends IAppExtended = IAppExtended>(
28
32
  market: string,
@@ -30,49 +34,51 @@ export async function getLayout<T extends IAppExtended = IAppExtended>(
30
34
  payload: MixerPayload,
31
35
  user: User,
32
36
  mixerContext: MixerContext,
33
- draft?: boolean
37
+ readOptions?: Pick<ReadOptions, 'depth' | 'draft'> | boolean
34
38
  ): Promise<ILayout<T>> {
35
- const base = buildBaseOptions(user, locale, draft);
39
+ const options = typeof readOptions === 'object' ? readOptions : { draft: readOptions };
40
+ const base = buildReadOptions({
41
+ user,
42
+ locale,
43
+ draft: options.draft,
44
+ depth: options.depth ?? DEFAULT_LAYOUT_DEPTH,
45
+ });
36
46
  const findOpts = { ...base, pagination: false, limit: 10000 } as const;
37
47
 
38
- // markets
39
- const marketsResult = await payload.find({
40
- ...findOpts,
41
- collection: 'market' as CollectionSlug,
42
- context: mixerContext,
43
- });
44
- const markets = marketsResult.docs as unknown as IMarket[];
48
+ // independent reads: markets, locales (global with an `items` array), labels, app global, menus
49
+ const [marketsResult, localeGlobal, labelsResult, app, menusResult] = await Promise.all([
50
+ payload.find({
51
+ ...findOpts,
52
+ collection: 'market' as CollectionSlug,
53
+ context: mixerContext,
54
+ }),
55
+ payload.findGlobal({
56
+ ...base,
57
+ slug: 'locale' as GlobalSlug,
58
+ context: mixerContext,
59
+ }) as unknown as Promise<{ items: ILocale[] }>,
60
+ payload.find({
61
+ ...findOpts,
62
+ collection: 'label' as CollectionSlug,
63
+ context: mixerContext,
64
+ }),
65
+ // app global (slug 'app', registered by payload-plugin-bowl)
66
+ payload.findGlobal({
67
+ ...base,
68
+ slug: 'app' as GlobalSlug,
69
+ context: mixerContext,
70
+ }) as unknown as Promise<IApp<T>>,
71
+ payload.find({
72
+ ...findOpts,
73
+ collection: 'menu' as CollectionSlug,
74
+ context: mixerContext,
75
+ }),
76
+ ]);
45
77
 
46
- // locales (global with an `items` array)
47
- const localeGlobal = await payload.findGlobal({
48
- ...base,
49
- slug: 'locale' as GlobalSlug,
50
- context: mixerContext,
51
- }) as unknown as { items: ILocale[] };
78
+ const markets = marketsResult.docs as unknown as IMarket[];
52
79
  const locales = (localeGlobal?.items ?? []).filter(x => x.isActive);
53
-
54
- // labels
55
- const labelsResult = await payload.find({
56
- ...findOpts,
57
- collection: 'label' as CollectionSlug,
58
- context: mixerContext,
59
- });
60
80
  const labels = labelsResult.docs as unknown as ILabel[];
61
81
 
62
- // app global (slug 'app', registered by payload-plugin-bowl)
63
- const app = await payload.findGlobal({
64
- ...base,
65
- slug: 'app' as GlobalSlug,
66
- context: mixerContext,
67
- }) as unknown as IApp<T>;
68
-
69
- // menus
70
- const menusResult = await payload.find({
71
- ...findOpts,
72
- collection: 'menu' as CollectionSlug,
73
- context: mixerContext,
74
- });
75
-
76
82
  const menu: Record<string, ILocalizedMenu> = {};
77
83
  (menusResult.docs as unknown as ILocalizedMenu[]).forEach(x => {
78
84
  menu[String(x.id)] = x;
@@ -1,4 +1,4 @@
1
- 'use server';
1
+ import 'server-only';
2
2
 
3
3
  import type { IComponent } from '@websolutespa/bom-core';
4
4
  import type { ILazyComponent, ILazyStaticProps, ILazyStaticPropsFunc } from '../types';