@enonic/nextjs-adapter 3.0.0-B2 → 3.0.0-B4

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 (173) hide show
  1. package/LICENSE +201 -0
  2. package/baseMappings.js.map +1 -1
  3. package/client.js.map +1 -1
  4. package/common/ComponentRegistry.js.map +1 -1
  5. package/common/UrlProcessor.js.map +1 -1
  6. package/common/constants.js.map +1 -1
  7. package/common/env.js +14 -13
  8. package/common/env.js.map +1 -1
  9. package/guillotine/applyProcessors.js.map +1 -1
  10. package/guillotine/buildComponentTree.js.map +1 -1
  11. package/guillotine/buildErrorResponse.js.map +1 -1
  12. package/guillotine/buildPage.js.map +1 -1
  13. package/guillotine/collectComponentDescriptors.js.map +1 -1
  14. package/guillotine/combineMultipleQueries.js.map +1 -1
  15. package/guillotine/createMetaData.js.map +1 -1
  16. package/guillotine/fetchContent.d.ts +2 -2
  17. package/guillotine/fetchContent.js +172 -170
  18. package/guillotine/fetchContent.js.map +1 -1
  19. package/guillotine/fetchContentData.js.map +1 -1
  20. package/guillotine/fetchContentPathsForAllLocales.js.map +1 -1
  21. package/guillotine/fetchContentPathsForLocale.js.map +1 -1
  22. package/guillotine/fetchFromApi.d.ts +1 -1
  23. package/guillotine/fetchFromApi.js +56 -54
  24. package/guillotine/fetchFromApi.js.map +1 -1
  25. package/guillotine/fetchGuillotine.d.ts +1 -1
  26. package/guillotine/fetchGuillotine.js +38 -36
  27. package/guillotine/fetchGuillotine.js.map +1 -1
  28. package/guillotine/fetchMetaData.js.map +1 -1
  29. package/guillotine/getCleanContentPathArrayOrThrow400.js.map +1 -1
  30. package/guillotine/getComponentConfig.js.map +1 -1
  31. package/guillotine/getContentAndComponentPaths.js.map +1 -1
  32. package/guillotine/getMetaData.js.map +1 -1
  33. package/guillotine/getParentRegion.js.map +1 -1
  34. package/guillotine/getQueryAndVariables.js.map +1 -1
  35. package/guillotine/metadata/configQuery.js.map +1 -1
  36. package/guillotine/metadata/richTextQuery.js.map +1 -1
  37. package/guillotine/parseComponentPath.js.map +1 -1
  38. package/guillotine/prefixLayoutPath.js.map +1 -1
  39. package/guillotine/processComponentConfig.js.map +1 -1
  40. package/guillotine/restrictComponentsToPath.js.map +1 -1
  41. package/guillotine/validateData.js.map +1 -1
  42. package/guillotine/validateNotFound.js.map +1 -1
  43. package/guillotine/validateShortcut.js.map +1 -1
  44. package/i18n/LocaleContext.jsx.map +1 -1
  45. package/i18n/i18n.js.map +1 -1
  46. package/index.d.ts +1 -5
  47. package/index.js +1 -5
  48. package/index.js.map +1 -1
  49. package/package.json +15 -16
  50. package/query/Shortcut.js.map +1 -1
  51. package/server.d.ts +5 -0
  52. package/server.js +6 -0
  53. package/server.js.map +1 -0
  54. package/src/baseMappings.ts +53 -0
  55. package/src/client.ts +2 -0
  56. package/src/common/ComponentRegistry.ts +162 -0
  57. package/src/common/UrlProcessor.ts +142 -0
  58. package/src/common/constants.ts +85 -0
  59. package/src/common/env.ts +39 -0
  60. package/src/guillotine/applyProcessors.ts +38 -0
  61. package/src/guillotine/buildComponentTree.ts +63 -0
  62. package/src/guillotine/buildErrorResponse.ts +39 -0
  63. package/src/guillotine/buildPage.ts +15 -0
  64. package/src/guillotine/collectComponentDescriptors.ts +63 -0
  65. package/src/guillotine/combineMultipleQueries.ts +75 -0
  66. package/src/guillotine/createMetaData.ts +73 -0
  67. package/src/guillotine/fetchContent.ts +250 -0
  68. package/src/guillotine/fetchContentData.ts +33 -0
  69. package/src/guillotine/fetchContentPathsForAllLocales.ts +21 -0
  70. package/src/guillotine/fetchContentPathsForLocale.ts +42 -0
  71. package/src/guillotine/fetchFromApi.ts +69 -0
  72. package/src/guillotine/fetchGuillotine.ts +61 -0
  73. package/src/guillotine/fetchMetaData.ts +32 -0
  74. package/src/guillotine/getCleanContentPathArrayOrThrow400.ts +22 -0
  75. package/src/guillotine/getComponentConfig.ts +7 -0
  76. package/src/guillotine/getContentAndComponentPaths.ts +11 -0
  77. package/src/guillotine/getMetaData.ts +73 -0
  78. package/src/guillotine/getParentRegion.ts +59 -0
  79. package/src/guillotine/getQueryAndVariables.ts +52 -0
  80. package/src/guillotine/metadata/configQuery.ts +46 -0
  81. package/src/guillotine/metadata/richTextQuery.ts +34 -0
  82. package/src/guillotine/parseComponentPath.ts +29 -0
  83. package/src/guillotine/prefixLayoutPath.ts +16 -0
  84. package/src/guillotine/processComponentConfig.ts +38 -0
  85. package/src/guillotine/restrictComponentsToPath.ts +45 -0
  86. package/src/guillotine/validateData.ts +11 -0
  87. package/src/guillotine/validateNotFound.ts +20 -0
  88. package/src/guillotine/validateShortcut.ts +37 -0
  89. package/src/i18n/LocaleContext.tsx +54 -0
  90. package/src/i18n/i18n.ts +54 -0
  91. package/src/index.ts +42 -0
  92. package/src/query/Shortcut.ts +14 -0
  93. package/src/server.ts +5 -0
  94. package/src/types/component.ts +147 -0
  95. package/src/types/componentProps.ts +142 -0
  96. package/src/types/guillotine.ts +426 -0
  97. package/src/types/i18n.ts +8 -0
  98. package/src/types/index.ts +58 -0
  99. package/src/types/next.ts +7 -0
  100. package/src/types/util.ts +5 -0
  101. package/src/utils/addLineNumbers.ts +3 -0
  102. package/src/utils/buildGuillotineRequestHeaders.ts +32 -0
  103. package/src/utils/commonChars.ts +16 -0
  104. package/src/utils/fixDoubleSlashes.ts +3 -0
  105. package/src/utils/getContentApiUrl.ts +20 -0
  106. package/src/utils/getProjectLocaleConfig.ts +24 -0
  107. package/src/utils/getProjectLocaleConfigById.ts +21 -0
  108. package/src/utils/getProjectLocaleConfigByLocale.ts +19 -0
  109. package/src/utils/getProjectLocaleConfigs.ts +36 -0
  110. package/src/utils/getProjectLocales.ts +6 -0
  111. package/src/utils/getRenderMode.ts +14 -0
  112. package/src/utils/getRequestLocaleInfo.ts +29 -0
  113. package/src/utils/getXpBaseUrl.ts +12 -0
  114. package/src/utils/indent.ts +4 -0
  115. package/src/utils/sanitizeGraphqlName.ts +11 -0
  116. package/src/views/BaseComponent.tsx +153 -0
  117. package/src/views/BaseContent.tsx +47 -0
  118. package/src/views/BaseLayout.tsx +33 -0
  119. package/src/views/BaseMacro.tsx +94 -0
  120. package/src/views/BasePage.tsx +42 -0
  121. package/src/views/BasePart.tsx +34 -0
  122. package/src/views/Empty.tsx +5 -0
  123. package/src/views/Fragment.tsx +41 -0
  124. package/src/views/MainView.tsx +22 -0
  125. package/src/views/PropsView.tsx +38 -0
  126. package/src/views/Region.tsx +76 -0
  127. package/src/views/RichTextView.tsx +75 -0
  128. package/src/views/SingleComponent.tsx +19 -0
  129. package/src/views/StaticContent.tsx +39 -0
  130. package/src/views/Text.tsx +19 -0
  131. package/src/views/getClientHydratedHtml.tsx +12 -0
  132. package/src/views/macros/DefaultMacro.tsx +9 -0
  133. package/src/views/macros/DisableMacro.tsx +8 -0
  134. package/types/component.js.map +1 -1
  135. package/types/componentProps.js.map +1 -1
  136. package/types/guillotine.js.map +1 -1
  137. package/types/i18n.js.map +1 -1
  138. package/types/index.js.map +1 -1
  139. package/types/next.js.map +1 -1
  140. package/types/util.js.map +1 -1
  141. package/utils/addLineNumbers.js.map +1 -1
  142. package/utils/buildGuillotineRequestHeaders.js.map +1 -1
  143. package/utils/commonChars.js.map +1 -1
  144. package/utils/fixDoubleSlashes.js.map +1 -1
  145. package/utils/getContentApiUrl.js.map +1 -1
  146. package/utils/getProjectLocaleConfig.js.map +1 -1
  147. package/utils/getProjectLocaleConfigById.js.map +1 -1
  148. package/utils/getProjectLocaleConfigByLocale.js.map +1 -1
  149. package/utils/getProjectLocaleConfigs.js.map +1 -1
  150. package/utils/getProjectLocales.js.map +1 -1
  151. package/utils/getRenderMode.js.map +1 -1
  152. package/utils/getRequestLocaleInfo.js.map +1 -1
  153. package/utils/getXpBaseUrl.js.map +1 -1
  154. package/utils/indent.js.map +1 -1
  155. package/utils/sanitizeGraphqlName.js.map +1 -1
  156. package/views/BaseComponent.jsx.map +1 -1
  157. package/views/BaseContent.jsx.map +1 -1
  158. package/views/BaseLayout.jsx.map +1 -1
  159. package/views/BaseMacro.jsx.map +1 -1
  160. package/views/BasePage.jsx.map +1 -1
  161. package/views/BasePart.jsx.map +1 -1
  162. package/views/Empty.jsx.map +1 -1
  163. package/views/Fragment.jsx.map +1 -1
  164. package/views/MainView.jsx.map +1 -1
  165. package/views/PropsView.jsx.map +1 -1
  166. package/views/Region.jsx.map +1 -1
  167. package/views/RichTextView.jsx.map +1 -1
  168. package/views/SingleComponent.jsx.map +1 -1
  169. package/views/StaticContent.jsx.map +1 -1
  170. package/views/Text.jsx.map +1 -1
  171. package/views/getClientHydratedHtml.jsx.map +1 -1
  172. package/views/macros/DefaultMacro.jsx.map +1 -1
  173. package/views/macros/DisableMacro.jsx.map +1 -1
@@ -0,0 +1,142 @@
1
+ import type {ImageData, LinkData, MetaData} from '../types';
2
+
3
+
4
+ import {RENDER_MODE} from './constants';
5
+ import {fixDoubleSlashes} from '../utils/fixDoubleSlashes';
6
+ import {getProjectLocaleConfigById} from '../utils/getProjectLocaleConfigById';
7
+ import {commonChars} from '../utils/commonChars';
8
+ import {addBasePath} from 'next/dist/client/add-base-path';
9
+
10
+
11
+ export class UrlProcessor {
12
+
13
+ public static IMG_TAG = 'img';
14
+ public static LINK_TAG = 'a';
15
+ public static MACRO_TAG = 'editor-macro';
16
+
17
+ public static IMG_ATTR = 'data-image-ref';
18
+ public static LINK_ATTR = 'data-link-ref';
19
+ public static MACRO_ATTR = 'data-macro-ref';
20
+
21
+ private static IMG_ATMT_REGEXP = /_\/image\/|_\/attachment\//;
22
+ private static endSlashPattern = /\/+$/;
23
+ private static startSlashPattern = /^\/+/;
24
+ private static localhostPattern = /localhost/;
25
+
26
+ private static siteKey: string;
27
+
28
+ public static process(url: string, meta: MetaData, serverSide = false, isResource = false): string {
29
+ if (this.startsWithHash(url) || !meta || (this.isAttachmentUrl(url)) && meta.renderMode === RENDER_MODE.NEXT) {
30
+ // do not process if:
31
+ // - url starts with #
32
+ // - meta is absent
33
+ // - attachment urls in NEXT mode
34
+ return url;
35
+ }
36
+
37
+ let normalUrl: string;
38
+ if (this.isRelative(url)) {
39
+ normalUrl = url;
40
+ } else {
41
+ // url is absolute, try to make it relative by striping apiUrl
42
+ // NB: will fail if content api is not on the same domain as enonic xp
43
+ const apiUrl = this.getApiUrl(meta);
44
+ normalUrl = this.stripApiUrl(url, apiUrl);
45
+ }
46
+
47
+ const baseUrl = meta?.baseUrl && meta?.baseUrl !== '/' ? meta.baseUrl : '';
48
+
49
+ let result: string;
50
+ if (meta.renderMode === RENDER_MODE.NEXT) {
51
+ // only add basePath and locale in next mode
52
+ result = `/${normalUrl}`;
53
+ if (!isResource && meta.locale !== meta.defaultLocale) {
54
+ // append locale if it's not the default one
55
+ // to avoid additional middleware redirection
56
+ // NB: don't add locale to resource urls
57
+ result = `/${meta.locale}${result}`;
58
+ }
59
+ if (!serverSide) {
60
+ // no need for baseurl and basepath on server
61
+ result = addBasePath(`${baseUrl}${result}`);
62
+ }
63
+ } else {
64
+ result = `${baseUrl}/${normalUrl}`;
65
+ }
66
+
67
+ return fixDoubleSlashes(result);
68
+ }
69
+
70
+ public static setSiteKey(key: string): void {
71
+ this.siteKey = key;
72
+ }
73
+
74
+ public static isMediaLink(ref: string, linkData: LinkData[]): boolean {
75
+ // Assuming linkData can't contain more than one entry with the same ref
76
+ return Array.isArray(linkData) && !!linkData.find(data => data?.ref === ref)?.media;
77
+ }
78
+
79
+ public static isContentImage(ref: string, imageData: ImageData[]): boolean {
80
+ // Assuming imageData can't contain more than one entry with the same ref
81
+ return Array.isArray(imageData) && !!imageData.find(data => data.ref === ref)?.image;
82
+ }
83
+
84
+ public static processSrcSet(srcset: string, meta: MetaData): string {
85
+ return srcset.split(/, */g).map(src => {
86
+ const srcParts = src.trim().split(' ');
87
+ switch (srcParts.length) {
88
+ case 1: // src only
89
+ return this.process(src, meta);
90
+ case 2: // width descriptor
91
+ return `${this.process(srcParts[0], meta)} ${srcParts[1]}`;
92
+ case 3: // pixel density descriptor
93
+ return `${this.process(srcParts[0], meta)} ${srcParts[1]} ${srcParts[2]}`;
94
+ default:
95
+ console.warn('Can not process image srcset: ' + src);
96
+ return src;
97
+ }
98
+ }).join(', ');
99
+ }
100
+
101
+ private static stripApiUrl(url: string, apiUrl: string): string {
102
+ // normalise localhost-127.0.0.1 if present in urls
103
+ const normalUrl = url.replace(this.localhostPattern, '127.0.0.1');
104
+ const normalApiUrl = apiUrl.replace(this.localhostPattern, '127.0.0.1');
105
+ const common = commonChars(normalUrl, normalApiUrl);
106
+ const remaining = common.length > 0 ? normalUrl.substring(common.length) : normalUrl;
107
+ return (remaining.length > 0 && remaining.charAt(0) === '/') ? remaining.substring(1) : remaining;
108
+ }
109
+
110
+ private static isAttachmentUrl(url: string): boolean {
111
+ return this.IMG_ATMT_REGEXP.test(url);
112
+ }
113
+
114
+ private static startsWithHash(url: string): boolean {
115
+ return url?.charAt(0) == '#';
116
+ }
117
+
118
+ private static isRelative(url: string): boolean {
119
+ return !/^(?:ht|f)tps?:\/\/[^ :\r\n\t]+/.test(url);
120
+ }
121
+
122
+ private static getApiUrl(meta: MetaData) {
123
+ let combinedUrl = meta.apiUrl?.replace(this.endSlashPattern, '') || '';
124
+ if (this.siteKey?.length) {
125
+ combinedUrl += '/' + this.siteKey.replace(this.startSlashPattern, '');
126
+ }
127
+ return combinedUrl;
128
+ }
129
+ }
130
+
131
+ // Actual site is going to be set in fetchContent method, but set a default fallback here too
132
+ const defaultConfig = getProjectLocaleConfigById();
133
+ UrlProcessor.setSiteKey(defaultConfig.site);
134
+
135
+ export function getUrl(url: string, meta: MetaData): string {
136
+ return UrlProcessor.process(url, meta, false, false);
137
+ }
138
+
139
+ export function getAsset(url: string, meta: MetaData): string {
140
+ return UrlProcessor.process(url, meta, false, true);
141
+ }
142
+
@@ -0,0 +1,85 @@
1
+ export const CATCH_ALL = '*';
2
+
3
+ export enum ENV_VARS {
4
+ PROJECTS = 'ENONIC_PROJECTS',
5
+ API_URL = 'ENONIC_API',
6
+ APP_NAME = 'ENONIC_APP_NAME',
7
+ }
8
+
9
+ export const FRAGMENT_CONTENTTYPE_NAME = 'portal:fragment';
10
+ export const FRAGMENT_DEFAULT_REGION_NAME = 'fragment';
11
+
12
+ /** excluded contents:
13
+ * - folder
14
+ * - shortcuts (redirect can't be used in prerender)
15
+ * - media
16
+ * - paths with '/_' in them
17
+ */
18
+ export const GET_STATIC_PATHS_QUERY = `query ($count: Int) {
19
+ guillotine {
20
+ queryDsl(
21
+ first: $count,
22
+ sort: {
23
+ field: "modifiedTime",
24
+ direction: DESC
25
+ }
26
+ query: {boolean: {mustNot: [
27
+ {in: {field: "type", stringValues: ["base:folder", "base:shortcut"]}}
28
+ {like: {field: "type", value: "media:*"}}
29
+ {like: {field: "_path", value: "*/_*"}}
30
+ ]}}
31
+ ) {
32
+ _name
33
+ _path
34
+ site {_name}
35
+ }
36
+ }
37
+ }`;
38
+
39
+ // Seems like NodeJS.fetch lowercases all headers, so we need to lowercase the
40
+ // header names here.
41
+ export enum GUILLOTINE_REQUEST_HEADERS {
42
+ COOKIE = 'Cookie',
43
+ LOCALE = 'locale',
44
+ LOCALES = 'locales',
45
+ DEFAULT_LOCALE = 'default-locale',
46
+ }
47
+
48
+ export const JSESSIONID_HEADER = 'jsessionid';
49
+
50
+ export const PAGE_TEMPLATE_CONTENTTYPE_NAME = 'portal:page-template';
51
+ export const PAGE_TEMPLATE_FOLDER = 'portal:template-folder';
52
+ export const PORTAL_COMPONENT_ATTRIBUTE = 'data-portal-component-type';
53
+ export const PORTAL_REGION_ATTRIBUTE = 'data-portal-region';
54
+ export const PROJECT_ID_HEADER = 'content-studio-project';
55
+
56
+ export enum RENDER_MODE {
57
+ INLINE = 'inline',
58
+ EDIT = 'edit',
59
+ PREVIEW = 'preview',
60
+ LIVE = 'live',
61
+ ADMIN = 'admin',
62
+ NEXT = 'next' // Fallback: not using XP proxy but rendering directly with next.js
63
+ }
64
+
65
+ export const RENDER_MODE_HEADER = 'content-studio-mode';
66
+
67
+ export const SITE_CONTENTTYPE_NAME = 'portal:site';
68
+
69
+ // URI parameter marking that a request is for a preview for CS. MUST MATCH THE VALUE OF 'FROM_XP_PARAM' on XP side.
70
+ export const XP_BASE_URL_HEADER = 'xpbaseurl';
71
+
72
+ // TODO: Use these instead of hardcoded strings everywhere
73
+ export enum XP_COMPONENT_TYPE {
74
+ PART = 'part',
75
+ LAYOUT = 'layout',
76
+ TEXT = 'text',
77
+ FRAGMENT = 'fragment',
78
+ PAGE = 'page',
79
+ }
80
+
81
+ export enum XP_REQUEST_TYPE {
82
+ COMPONENT = 'component',
83
+ TYPE = 'type',
84
+ PAGE = 'page',
85
+ }
@@ -0,0 +1,39 @@
1
+ import {ENV_VARS} from './constants';
2
+
3
+ const isServer = typeof window === 'undefined';
4
+
5
+ // IMPORTANT:
6
+ // NEXT_PUBLIC_ vars should be explicitly referenced to be made available on the client side (substituted with constants) !!!
7
+ /** URL to the guillotine API */
8
+ export const API_URL = isServer ? process.env[ENV_VARS.API_URL] : process.env['NEXT_PUBLIC_ENONIC_API'];
9
+
10
+ /** Optional utility value - defining in one place the name of the target app (the app that defines the content types, the app name is therefore part of the content type strings used both in typeselector and in query introspections) */
11
+ export const APP_NAME = isServer ? process.env[ENV_VARS.APP_NAME] : process.env['NEXT_PUBLIC_ENONIC_APP_NAME'];
12
+
13
+ /** Optional utility value - derived from APP_NAME, only with underscores instead of dots */
14
+ export const APP_NAME_UNDERSCORED = (APP_NAME || '').replace(/\./g, '_');
15
+
16
+ /** Optional utility value - derived from APP_NAME, only with dashes instead of dots */
17
+ export const APP_NAME_DASHED = (APP_NAME || '').replace(/\./g, '-');
18
+
19
+ const mode = isServer ? process.env.MODE : process.env.NEXT_PUBLIC_MODE;
20
+
21
+ export const IS_DEV_MODE = (mode === 'development');
22
+
23
+ /** Locales and Enonic XP projects correspondence list */
24
+ export const PROJECTS = isServer ? process.env[ENV_VARS.PROJECTS] : process.env['NEXT_PUBLIC_ENONIC_PROJECTS'];
25
+
26
+ const requiredConstants = {
27
+ [ENV_VARS.APP_NAME]: APP_NAME,
28
+ [ENV_VARS.API_URL]: API_URL,
29
+ [ENV_VARS.PROJECTS]: PROJECTS,
30
+ };
31
+
32
+ // Verify required values on server-side only
33
+ Object.keys(requiredConstants).forEach((key: string) => {
34
+ const val = requiredConstants[key];
35
+ if (!val) {
36
+ throw new Error(`Environment variable '${isServer ? '' : 'NEXT_PUBLIC_'}${key}' is missing (from .env?)`);
37
+ }
38
+ });
39
+
@@ -0,0 +1,38 @@
1
+ import type {
2
+ ComponentDescriptor,
3
+ ContentResult,
4
+ Context,
5
+ } from '../types';
6
+
7
+
8
+ import {getComponentConfig} from './getComponentConfig';
9
+
10
+
11
+ const NO_PROPS_PROCESSOR = async (props: any) => await props ?? {};
12
+
13
+
14
+ export async function applyProcessors(
15
+ componentDescriptors: ComponentDescriptor[],
16
+ contentResults: ContentResult,
17
+ context: Context,
18
+ ): Promise < PromiseSettledResult < any > [] > {
19
+
20
+ let dataCounter = 0;
21
+ const processorPromises = componentDescriptors.map(async (desc: ComponentDescriptor) => {
22
+ // we're iterating component descriptors here
23
+ // some of them might not have provided graphql requests
24
+ // but we still need to run props processor for them
25
+ // in case they want to fetch their data from elsewhere
26
+ const propsProcessor = desc.type?.processor || NO_PROPS_PROCESSOR;
27
+ let data;
28
+ if (desc.queryAndVariables) {
29
+ // if there is a query then there must be a result for it
30
+ data = contentResults.contents[dataCounter++];
31
+ }
32
+
33
+ const config = getComponentConfig(desc.component);
34
+ return await propsProcessor(data, context, config);
35
+ });
36
+
37
+ return Promise.allSettled(processorPromises);
38
+ }
@@ -0,0 +1,63 @@
1
+ import type {
2
+ PageComponent,
3
+ RegionTree,
4
+ } from '../types';
5
+
6
+
7
+ import {
8
+ FRAGMENT_CONTENTTYPE_NAME,
9
+ XP_COMPONENT_TYPE,
10
+ } from '../common/constants';
11
+ import {parseComponentPath} from './parseComponentPath';
12
+ import {getParentRegion} from './getParentRegion';
13
+
14
+
15
+ export function buildComponentTree(
16
+ comps: PageComponent[],
17
+ rootComp: PageComponent,
18
+ contentType?: string,
19
+ ): PageComponent {
20
+ const rootTree: RegionTree = {};
21
+ const isFragmentType = contentType === FRAGMENT_CONTENTTYPE_NAME;
22
+
23
+ if (isFragmentType) {
24
+ rootComp.regions = rootTree;
25
+ }
26
+
27
+ (comps || []).forEach(cmp => {
28
+ const cmpPath = parseComponentPath(contentType, cmp.path);
29
+
30
+ if (cmp.path === '/') {
31
+ if (cmp.type === XP_COMPONENT_TYPE.PAGE && rootComp.type === XP_COMPONENT_TYPE.PAGE) {
32
+ // add page values to page object
33
+ rootComp = Object.assign(rootComp, cmp);
34
+ rootComp.page.regions = rootTree;
35
+ return;
36
+ } else if (cmp.type === XP_COMPONENT_TYPE.LAYOUT && rootComp.type === XP_COMPONENT_TYPE.FRAGMENT) {
37
+ // when rendering fragment as part of the page with layout at root path
38
+ // remove other children from list as they will be included in layout
39
+ rootComp.fragment.fragment.components = [cmp];
40
+ cmp.regions = rootTree;
41
+ }
42
+ }
43
+
44
+ if (cmp.type === XP_COMPONENT_TYPE.FRAGMENT) {
45
+ // build component subtree for fragment
46
+ cmp = buildComponentTree(cmp.fragment?.fragment?.components, cmp, contentType);
47
+ }
48
+
49
+ const region = getParentRegion(rootTree, contentType, cmpPath, comps, true);
50
+
51
+ if (region) {
52
+ // getting the index of component from string like '/main/0/left/11'
53
+ const cmpIndex = cmpPath[cmpPath.length - 1]?.index;
54
+ if (cmpIndex >= 0) {
55
+ region.components.splice(cmpIndex, 0, cmp);
56
+ } else {
57
+ throw Error(`Could not find [${cmp.type}] component index at ${JSON.stringify(cmpPath, null, 2)}, rendering not possible.`);
58
+ }
59
+ }
60
+ });
61
+
62
+ return rootComp;
63
+ }
@@ -0,0 +1,39 @@
1
+ import type {
2
+ FetchContentResult,
3
+ } from '../types';
4
+
5
+
6
+ import {
7
+ RENDER_MODE,
8
+ XP_REQUEST_TYPE,
9
+ } from '../common/constants';
10
+
11
+
12
+ export function buildErrorResponse(requestType: XP_REQUEST_TYPE, renderMode: RENDER_MODE,
13
+ apiUrl: string, baseUrl: string, locale: string, defaultLocale: string,
14
+ contentPath ? : string): (code ? : string, message ? : string) => FetchContentResult {
15
+
16
+ return function (code = '500', message = 'Unknown error') {
17
+ return {
18
+ error: {
19
+ code,
20
+ message,
21
+ },
22
+ page: null,
23
+ common: null,
24
+ data: null,
25
+ meta: {
26
+ type: '',
27
+ requestType: requestType,
28
+ renderMode: renderMode,
29
+ path: contentPath || '',
30
+ canRender: false,
31
+ catchAll: false,
32
+ apiUrl,
33
+ baseUrl,
34
+ locale,
35
+ defaultLocale,
36
+ },
37
+ };
38
+ };
39
+ }
@@ -0,0 +1,15 @@
1
+ import type {PageComponent} from '../types';
2
+
3
+
4
+ import {XP_COMPONENT_TYPE} from '../common/constants';
5
+ import {buildComponentTree} from './buildComponentTree';
6
+
7
+
8
+ export function buildPage(contentType: string, comps: PageComponent[] = []): PageComponent {
9
+ const page: PageComponent = {
10
+ type: XP_COMPONENT_TYPE.PAGE,
11
+ path: '/',
12
+ };
13
+
14
+ return buildComponentTree(comps, page, contentType);
15
+ }
@@ -0,0 +1,63 @@
1
+ import type {
2
+ ComponentDescriptor,
3
+ Context,
4
+ PageComponent,
5
+ } from '../types';
6
+
7
+
8
+ import {ComponentRegistry} from '../common/ComponentRegistry';
9
+ import {XP_COMPONENT_TYPE} from '../common/constants';
10
+ import {
11
+ APP_NAME,
12
+ APP_NAME_DASHED,
13
+ } from '../common/env';
14
+ import {processComponentConfig} from './processComponentConfig';
15
+ import {getComponentConfig} from './getComponentConfig';
16
+ import {getQueryAndVariables} from './getQueryAndVariables';
17
+
18
+
19
+ export function collectComponentDescriptors({
20
+ components,
21
+ xpContentPath,
22
+ context,
23
+ }: {
24
+ components: PageComponent[]
25
+ xpContentPath: string
26
+ context: Context
27
+ }): ComponentDescriptor[] {
28
+
29
+ const descriptors: ComponentDescriptor[] = [];
30
+
31
+ for (const cmp of (components || [])) {
32
+ processComponentConfig(APP_NAME, APP_NAME_DASHED, cmp);
33
+ // only look for parts
34
+ // look for single part if it is a single component request
35
+ if (XP_COMPONENT_TYPE.FRAGMENT !== cmp.type) {
36
+ const cmpDef = ComponentRegistry.getByComponent(cmp);
37
+ if (cmpDef) {
38
+ // const partPath = `${xpContentPath}/_component${cmp.path}`;
39
+ const config = getComponentConfig(cmp);
40
+ const queryAndVariables = getQueryAndVariables(cmp.type, xpContentPath, context, cmpDef.query, config);
41
+ if (queryAndVariables) {
42
+ descriptors.push({
43
+ component: cmp,
44
+ type: cmpDef,
45
+ queryAndVariables: queryAndVariables,
46
+ });
47
+ }
48
+ }
49
+ } else {
50
+ // look for parts inside fragments
51
+ const fragPartDescs = collectComponentDescriptors({
52
+ components: cmp.fragment?.fragment?.components,
53
+ xpContentPath,
54
+ context,
55
+ });
56
+ if (fragPartDescs.length) {
57
+ descriptors.push(...fragPartDescs);
58
+ }
59
+ }
60
+ }
61
+
62
+ return descriptors;
63
+ }
@@ -0,0 +1,75 @@
1
+ import type {
2
+ ComponentDescriptor,
3
+ QueryAndVariables,
4
+ } from '../types';
5
+
6
+
7
+ const ALIAS_PREFIX = 'request';
8
+
9
+ const GRAPHQL_FRAGMENTS_REGEXP = /fragment\s+.+\s+on\s+.+\s*{[\s\w{}().,:"'`]+}/;
10
+
11
+ const GUILLOTINE_QUERY_REGEXP = /^\s*query\s*(?:\((.*)*\))?\s*{\s*guillotine\s*{((?:.|\s)+)}\s*}\s*$/;
12
+
13
+
14
+ export function combineMultipleQueries(queriesWithVars: ComponentDescriptor[]): QueryAndVariables {
15
+ const queries: string[] = [];
16
+ const fragments: string[] = [];
17
+ const superVars: Record<string,any> = {};
18
+ const superParams: string[] = [];
19
+
20
+ queriesWithVars.forEach((componentDescriptor: ComponentDescriptor, index: number) => {
21
+ const queryAndVars = componentDescriptor.queryAndVariables;
22
+ if (!queryAndVars) {
23
+ return;
24
+ }
25
+
26
+ // Extract fragments first if exist
27
+ let q = queryAndVars.query;
28
+ let match = q.match(GRAPHQL_FRAGMENTS_REGEXP);
29
+ if (match?.length === 1) {
30
+ // extract a fragment to put it at root level
31
+ fragments.push(match[0]);
32
+ // remove it from query because queries are going to get wrapped
33
+ q = q.replace(match[0], '');
34
+ }
35
+
36
+ // Extract graphql query and its params and add prefixes to exclude collisions with other queries
37
+ match = q.match(GUILLOTINE_QUERY_REGEXP) || ['']; // Fall back to empty string if no match
38
+ const args = match[1];
39
+ let query = match[2];
40
+
41
+ if (args) {
42
+ args.split(',').forEach(originalParamString => {
43
+ const [originalKey, originalVal] = originalParamString.trim().split(':');
44
+ const [prefixedKey, prefixedVal] = [`$${ALIAS_PREFIX}${index}_${originalKey.substr(1)}`, originalVal];
45
+ superParams.push(`${prefixedKey}:${prefixedVal}`);
46
+ // also update param references in query itself !
47
+ // query = query.replaceAll(originalKey, prefixedKey);
48
+ // replaceAll is not supported in older nodejs versions
49
+ const origKeyPattern = new RegExp(originalKey.replace(/\$/g, '\\$'), 'g');
50
+ query = query.replace(origKeyPattern, prefixedKey);
51
+ });
52
+ }
53
+
54
+ if (query?.length) {
55
+ queries.push(`${ALIAS_PREFIX}${index}:guillotine {${query}}`);
56
+ }
57
+
58
+ // Update variables with the same prefixes
59
+ Object.entries(queryAndVars.variables || {}).forEach(entry => {
60
+ superVars[`${ALIAS_PREFIX}${index}_${entry[0]}`] = entry[1];
61
+ });
62
+ });
63
+
64
+ // Compose the super query
65
+ const superQuery = `query ${superParams.length ? `(${superParams.join(', ')})` : ''} {
66
+ ${queries.join('\n')}
67
+ }
68
+ ${fragments.join('\n')}
69
+ `;
70
+
71
+ return {
72
+ query: superQuery,
73
+ variables: superVars,
74
+ };
75
+ }
@@ -0,0 +1,73 @@
1
+ import type {
2
+ ComponentDefinition,
3
+ MetaData,
4
+ PageComponent,
5
+ } from '../types';
6
+
7
+
8
+ import {RENDER_MODE, XP_REQUEST_TYPE} from '../common/constants';
9
+ import {ComponentRegistry} from '../common/ComponentRegistry';
10
+
11
+
12
+ export function createMetaData({
13
+ apiUrl,
14
+ baseUrl,
15
+ components = [], // Optional NOTE: Doesn't handle null
16
+ contentPath,
17
+ contentType,
18
+ defaultLocale,
19
+ locale,
20
+ pageCmp, // Optional
21
+ renderMode,
22
+ requestedComponentPath,
23
+ requestType,
24
+ }: {
25
+ apiUrl: string
26
+ baseUrl: string
27
+ components?: PageComponent[]
28
+ contentPath: string
29
+ contentType: string
30
+ defaultLocale: string
31
+ locale: string
32
+ pageCmp?: PageComponent
33
+ renderMode: RENDER_MODE
34
+ requestedComponentPath: string | undefined
35
+ requestType: XP_REQUEST_TYPE
36
+ }): MetaData {
37
+ // .meta will be visible in final rendered inline props.
38
+ // Only adding some .meta attributes here on certain conditions
39
+ // (instead of always adding them and letting them be visible as false/undefined etc)
40
+ const meta: MetaData = {
41
+ apiUrl,
42
+ baseUrl,
43
+ canRender: false,
44
+ catchAll: false, // catchAll only refers to content type catch-all
45
+ defaultLocale,
46
+ locale,
47
+ path: contentPath,
48
+ renderMode: renderMode,
49
+ requestType: requestType,
50
+ type: contentType,
51
+ };
52
+
53
+ if (requestedComponentPath) {
54
+ meta.requestedComponent = components.find(cmp => cmp.path === requestedComponentPath);
55
+ }
56
+
57
+ const pageDesc = pageCmp?.page?.descriptor;
58
+ const typeDef = ComponentRegistry.getContentType(contentType);
59
+ if (typeDef?.view && !typeDef.catchAll) {
60
+ meta.canRender = true;
61
+ } else if (requestType === XP_REQUEST_TYPE.COMPONENT) {
62
+ // always render a single component (show missing if not implemented)
63
+ meta.canRender = true;
64
+ } else if (pageDesc) {
65
+ // always render a page if there is a descriptor (show missing if not implemented)
66
+ meta.canRender = true;
67
+ } else if (typeDef?.view) {
68
+ meta.canRender = true;
69
+ meta.catchAll = true;
70
+ }
71
+
72
+ return meta;
73
+ }