@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.
- package/LICENSE +201 -0
- package/baseMappings.js.map +1 -1
- package/client.js.map +1 -1
- package/common/ComponentRegistry.js.map +1 -1
- package/common/UrlProcessor.js.map +1 -1
- package/common/constants.js.map +1 -1
- package/common/env.js +14 -13
- package/common/env.js.map +1 -1
- package/guillotine/applyProcessors.js.map +1 -1
- package/guillotine/buildComponentTree.js.map +1 -1
- package/guillotine/buildErrorResponse.js.map +1 -1
- package/guillotine/buildPage.js.map +1 -1
- package/guillotine/collectComponentDescriptors.js.map +1 -1
- package/guillotine/combineMultipleQueries.js.map +1 -1
- package/guillotine/createMetaData.js.map +1 -1
- package/guillotine/fetchContent.d.ts +2 -2
- package/guillotine/fetchContent.js +172 -170
- package/guillotine/fetchContent.js.map +1 -1
- package/guillotine/fetchContentData.js.map +1 -1
- package/guillotine/fetchContentPathsForAllLocales.js.map +1 -1
- package/guillotine/fetchContentPathsForLocale.js.map +1 -1
- package/guillotine/fetchFromApi.d.ts +1 -1
- package/guillotine/fetchFromApi.js +56 -54
- package/guillotine/fetchFromApi.js.map +1 -1
- package/guillotine/fetchGuillotine.d.ts +1 -1
- package/guillotine/fetchGuillotine.js +38 -36
- package/guillotine/fetchGuillotine.js.map +1 -1
- package/guillotine/fetchMetaData.js.map +1 -1
- package/guillotine/getCleanContentPathArrayOrThrow400.js.map +1 -1
- package/guillotine/getComponentConfig.js.map +1 -1
- package/guillotine/getContentAndComponentPaths.js.map +1 -1
- package/guillotine/getMetaData.js.map +1 -1
- package/guillotine/getParentRegion.js.map +1 -1
- package/guillotine/getQueryAndVariables.js.map +1 -1
- package/guillotine/metadata/configQuery.js.map +1 -1
- package/guillotine/metadata/richTextQuery.js.map +1 -1
- package/guillotine/parseComponentPath.js.map +1 -1
- package/guillotine/prefixLayoutPath.js.map +1 -1
- package/guillotine/processComponentConfig.js.map +1 -1
- package/guillotine/restrictComponentsToPath.js.map +1 -1
- package/guillotine/validateData.js.map +1 -1
- package/guillotine/validateNotFound.js.map +1 -1
- package/guillotine/validateShortcut.js.map +1 -1
- package/i18n/LocaleContext.jsx.map +1 -1
- package/i18n/i18n.js.map +1 -1
- package/index.d.ts +1 -5
- package/index.js +1 -5
- package/index.js.map +1 -1
- package/package.json +15 -16
- package/query/Shortcut.js.map +1 -1
- package/server.d.ts +5 -0
- package/server.js +6 -0
- package/server.js.map +1 -0
- package/src/baseMappings.ts +53 -0
- package/src/client.ts +2 -0
- package/src/common/ComponentRegistry.ts +162 -0
- package/src/common/UrlProcessor.ts +142 -0
- package/src/common/constants.ts +85 -0
- package/src/common/env.ts +39 -0
- package/src/guillotine/applyProcessors.ts +38 -0
- package/src/guillotine/buildComponentTree.ts +63 -0
- package/src/guillotine/buildErrorResponse.ts +39 -0
- package/src/guillotine/buildPage.ts +15 -0
- package/src/guillotine/collectComponentDescriptors.ts +63 -0
- package/src/guillotine/combineMultipleQueries.ts +75 -0
- package/src/guillotine/createMetaData.ts +73 -0
- package/src/guillotine/fetchContent.ts +250 -0
- package/src/guillotine/fetchContentData.ts +33 -0
- package/src/guillotine/fetchContentPathsForAllLocales.ts +21 -0
- package/src/guillotine/fetchContentPathsForLocale.ts +42 -0
- package/src/guillotine/fetchFromApi.ts +69 -0
- package/src/guillotine/fetchGuillotine.ts +61 -0
- package/src/guillotine/fetchMetaData.ts +32 -0
- package/src/guillotine/getCleanContentPathArrayOrThrow400.ts +22 -0
- package/src/guillotine/getComponentConfig.ts +7 -0
- package/src/guillotine/getContentAndComponentPaths.ts +11 -0
- package/src/guillotine/getMetaData.ts +73 -0
- package/src/guillotine/getParentRegion.ts +59 -0
- package/src/guillotine/getQueryAndVariables.ts +52 -0
- package/src/guillotine/metadata/configQuery.ts +46 -0
- package/src/guillotine/metadata/richTextQuery.ts +34 -0
- package/src/guillotine/parseComponentPath.ts +29 -0
- package/src/guillotine/prefixLayoutPath.ts +16 -0
- package/src/guillotine/processComponentConfig.ts +38 -0
- package/src/guillotine/restrictComponentsToPath.ts +45 -0
- package/src/guillotine/validateData.ts +11 -0
- package/src/guillotine/validateNotFound.ts +20 -0
- package/src/guillotine/validateShortcut.ts +37 -0
- package/src/i18n/LocaleContext.tsx +54 -0
- package/src/i18n/i18n.ts +54 -0
- package/src/index.ts +42 -0
- package/src/query/Shortcut.ts +14 -0
- package/src/server.ts +5 -0
- package/src/types/component.ts +147 -0
- package/src/types/componentProps.ts +142 -0
- package/src/types/guillotine.ts +426 -0
- package/src/types/i18n.ts +8 -0
- package/src/types/index.ts +58 -0
- package/src/types/next.ts +7 -0
- package/src/types/util.ts +5 -0
- package/src/utils/addLineNumbers.ts +3 -0
- package/src/utils/buildGuillotineRequestHeaders.ts +32 -0
- package/src/utils/commonChars.ts +16 -0
- package/src/utils/fixDoubleSlashes.ts +3 -0
- package/src/utils/getContentApiUrl.ts +20 -0
- package/src/utils/getProjectLocaleConfig.ts +24 -0
- package/src/utils/getProjectLocaleConfigById.ts +21 -0
- package/src/utils/getProjectLocaleConfigByLocale.ts +19 -0
- package/src/utils/getProjectLocaleConfigs.ts +36 -0
- package/src/utils/getProjectLocales.ts +6 -0
- package/src/utils/getRenderMode.ts +14 -0
- package/src/utils/getRequestLocaleInfo.ts +29 -0
- package/src/utils/getXpBaseUrl.ts +12 -0
- package/src/utils/indent.ts +4 -0
- package/src/utils/sanitizeGraphqlName.ts +11 -0
- package/src/views/BaseComponent.tsx +153 -0
- package/src/views/BaseContent.tsx +47 -0
- package/src/views/BaseLayout.tsx +33 -0
- package/src/views/BaseMacro.tsx +94 -0
- package/src/views/BasePage.tsx +42 -0
- package/src/views/BasePart.tsx +34 -0
- package/src/views/Empty.tsx +5 -0
- package/src/views/Fragment.tsx +41 -0
- package/src/views/MainView.tsx +22 -0
- package/src/views/PropsView.tsx +38 -0
- package/src/views/Region.tsx +76 -0
- package/src/views/RichTextView.tsx +75 -0
- package/src/views/SingleComponent.tsx +19 -0
- package/src/views/StaticContent.tsx +39 -0
- package/src/views/Text.tsx +19 -0
- package/src/views/getClientHydratedHtml.tsx +12 -0
- package/src/views/macros/DefaultMacro.tsx +9 -0
- package/src/views/macros/DisableMacro.tsx +8 -0
- package/types/component.js.map +1 -1
- package/types/componentProps.js.map +1 -1
- package/types/guillotine.js.map +1 -1
- package/types/i18n.js.map +1 -1
- package/types/index.js.map +1 -1
- package/types/next.js.map +1 -1
- package/types/util.js.map +1 -1
- package/utils/addLineNumbers.js.map +1 -1
- package/utils/buildGuillotineRequestHeaders.js.map +1 -1
- package/utils/commonChars.js.map +1 -1
- package/utils/fixDoubleSlashes.js.map +1 -1
- package/utils/getContentApiUrl.js.map +1 -1
- package/utils/getProjectLocaleConfig.js.map +1 -1
- package/utils/getProjectLocaleConfigById.js.map +1 -1
- package/utils/getProjectLocaleConfigByLocale.js.map +1 -1
- package/utils/getProjectLocaleConfigs.js.map +1 -1
- package/utils/getProjectLocales.js.map +1 -1
- package/utils/getRenderMode.js.map +1 -1
- package/utils/getRequestLocaleInfo.js.map +1 -1
- package/utils/getXpBaseUrl.js.map +1 -1
- package/utils/indent.js.map +1 -1
- package/utils/sanitizeGraphqlName.js.map +1 -1
- package/views/BaseComponent.jsx.map +1 -1
- package/views/BaseContent.jsx.map +1 -1
- package/views/BaseLayout.jsx.map +1 -1
- package/views/BaseMacro.jsx.map +1 -1
- package/views/BasePage.jsx.map +1 -1
- package/views/BasePart.jsx.map +1 -1
- package/views/Empty.jsx.map +1 -1
- package/views/Fragment.jsx.map +1 -1
- package/views/MainView.jsx.map +1 -1
- package/views/PropsView.jsx.map +1 -1
- package/views/Region.jsx.map +1 -1
- package/views/RichTextView.jsx.map +1 -1
- package/views/SingleComponent.jsx.map +1 -1
- package/views/StaticContent.jsx.map +1 -1
- package/views/Text.jsx.map +1 -1
- package/views/getClientHydratedHtml.jsx.map +1 -1
- package/views/macros/DefaultMacro.jsx.map +1 -1
- package/views/macros/DisableMacro.jsx.map +1 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Context,
|
|
3
|
+
QueryAndVariables,
|
|
4
|
+
SelectedQueryMaybeVariablesFunc,
|
|
5
|
+
} from '../types';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export function getQueryAndVariables(
|
|
9
|
+
type: string,
|
|
10
|
+
path: string,
|
|
11
|
+
context: Context,
|
|
12
|
+
selectedQuery?: SelectedQueryMaybeVariablesFunc,
|
|
13
|
+
config?: any,
|
|
14
|
+
): QueryAndVariables | undefined {
|
|
15
|
+
|
|
16
|
+
let query, getVariables;
|
|
17
|
+
|
|
18
|
+
if (
|
|
19
|
+
typeof selectedQuery === 'string' || typeof selectedQuery === 'function'
|
|
20
|
+
) {
|
|
21
|
+
query = selectedQuery;
|
|
22
|
+
|
|
23
|
+
} else if (Array.isArray(selectedQuery)) {
|
|
24
|
+
query = selectedQuery[0];
|
|
25
|
+
getVariables = selectedQuery[1];
|
|
26
|
+
|
|
27
|
+
} else if (typeof selectedQuery === 'object') {
|
|
28
|
+
query = selectedQuery.query;
|
|
29
|
+
getVariables = selectedQuery.variables;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (getVariables && typeof getVariables !== 'function') {
|
|
33
|
+
throw Error(`getVariables for content type ${type} should be a function, not: ${typeof getVariables}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (query && typeof query !== 'string' && typeof query !== 'function') {
|
|
37
|
+
throw Error(`Query for content type ${type} should be a string or function, not: ${typeof query}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof query === 'function') {
|
|
41
|
+
query = query(path, context, config);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (query) {
|
|
45
|
+
return {
|
|
46
|
+
query,
|
|
47
|
+
variables: getVariables ? getVariables(path, context, config) : {
|
|
48
|
+
path,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type {ComponentDefinition} from '../../types';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import {sanitizeGraphqlName} from '../../utils/sanitizeGraphqlName';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const configQuery = (
|
|
8
|
+
list: [string, ComponentDefinition][],
|
|
9
|
+
includeAppName = true,
|
|
10
|
+
canUseConfigAsJson = true,
|
|
11
|
+
): string => {
|
|
12
|
+
const hasQueryList = list?.filter(([key, def]) => def.configQuery) || [];
|
|
13
|
+
if (hasQueryList.length === 0) {
|
|
14
|
+
return canUseConfigAsJson ? 'configAsJson' : '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const configsByApp: Record<string,string[]> = {};
|
|
18
|
+
hasQueryList
|
|
19
|
+
.forEach((entry) => {
|
|
20
|
+
const nameParts = entry[0].split(':');
|
|
21
|
+
if (nameParts.length === 2) {
|
|
22
|
+
const sanitizedAppName = sanitizeGraphqlName(nameParts[0]);
|
|
23
|
+
let existingConfigs = configsByApp[sanitizedAppName];
|
|
24
|
+
if (!existingConfigs) {
|
|
25
|
+
existingConfigs = [];
|
|
26
|
+
configsByApp[sanitizedAppName] = existingConfigs;
|
|
27
|
+
}
|
|
28
|
+
existingConfigs.push(`${sanitizeGraphqlName(nameParts[1])}${entry[1].configQuery}`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Still query for configAsJson if at least one item has no configQuery defined
|
|
33
|
+
const configAsJsonQuery = canUseConfigAsJson && hasQueryList.length < list.length ? 'configAsJson' : '';
|
|
34
|
+
|
|
35
|
+
if (!includeAppName) {
|
|
36
|
+
return `${configAsJsonQuery}
|
|
37
|
+
config {
|
|
38
|
+
${Object.values(configsByApp).reduce((arr, curr) => arr.concat(curr), []).join('\n')}
|
|
39
|
+
}`;
|
|
40
|
+
} else {
|
|
41
|
+
return `${configAsJsonQuery}
|
|
42
|
+
config {
|
|
43
|
+
${Object.entries(configsByApp).map(entry => entry[0] + '{\n' + entry[1].join('\n') + '\n}')}
|
|
44
|
+
}`;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {ComponentRegistry} from '../../common/ComponentRegistry';
|
|
2
|
+
import {configQuery} from './configQuery';
|
|
3
|
+
import {indent} from '../../utils/indent';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const macroConfigQuery = (): string => {
|
|
7
|
+
return configQuery(ComponentRegistry.getMacros(), false, false);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const richTextQuery = (fieldName: string) => {
|
|
11
|
+
return `${fieldName}(processHtml:{type:absolute, imageWidths:[400, 800, 1200], imageSizes:"(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px"}) {
|
|
12
|
+
processedHtml
|
|
13
|
+
macros {
|
|
14
|
+
ref
|
|
15
|
+
name
|
|
16
|
+
descriptor
|
|
17
|
+
${indent(macroConfigQuery(), 8)}
|
|
18
|
+
}
|
|
19
|
+
links {
|
|
20
|
+
ref
|
|
21
|
+
media {
|
|
22
|
+
content {
|
|
23
|
+
_id
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
images {
|
|
28
|
+
ref
|
|
29
|
+
image {
|
|
30
|
+
_id
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}`;
|
|
34
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type {PathFragment} from '../types';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
FRAGMENT_CONTENTTYPE_NAME,
|
|
6
|
+
FRAGMENT_DEFAULT_REGION_NAME,
|
|
7
|
+
} from '../common/constants';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function parseComponentPath(contentType: string, path: string): PathFragment[] {
|
|
11
|
+
const matches: PathFragment[] = [];
|
|
12
|
+
let match;
|
|
13
|
+
const myRegexp = /(?:(\w+)\/(\d+))+/g;
|
|
14
|
+
while ((match = myRegexp.exec(path)) !== null) {
|
|
15
|
+
matches.push({
|
|
16
|
+
region: match[1],
|
|
17
|
+
index: +match[2],
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (contentType === FRAGMENT_CONTENTTYPE_NAME) {
|
|
21
|
+
// there is no main region in fragment content and the root component has '/' path
|
|
22
|
+
// prepend FRAGMENT_DEFAULT_REGION_NAME to path to conform to page structure
|
|
23
|
+
matches.unshift({
|
|
24
|
+
region: FRAGMENT_DEFAULT_REGION_NAME,
|
|
25
|
+
index: 0,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return matches;
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FRAGMENT_CONTENTTYPE_NAME,
|
|
3
|
+
FRAGMENT_DEFAULT_REGION_NAME,
|
|
4
|
+
} from '../common/constants';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export function prefixLayoutPath(contentType: string, path: string): string {
|
|
8
|
+
if (contentType !== FRAGMENT_CONTENTTYPE_NAME) {
|
|
9
|
+
return path;
|
|
10
|
+
} else {
|
|
11
|
+
// prepend FRAGMENT_DEFAULT_REGION_NAME to path to conform to page structure
|
|
12
|
+
// so that component with path '/' becomes /FRAGMENT_DEFAULT_REGION_NAME/0
|
|
13
|
+
// path /left/1 becomes /FRAGMENT_DEFAULT_REGION_NAME/0/left/1
|
|
14
|
+
return `/${FRAGMENT_DEFAULT_REGION_NAME}/0${path === '/' ? '' : path}`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type {PageComponent} from '../types';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import {sanitizeGraphqlName} from '../utils/sanitizeGraphqlName';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export function processComponentConfig(
|
|
8
|
+
myAppName: string,
|
|
9
|
+
myAppNameDashed: string,
|
|
10
|
+
cmp: PageComponent,
|
|
11
|
+
): void {
|
|
12
|
+
const cmpData = cmp[cmp.type];
|
|
13
|
+
if (cmpData && 'descriptor' in cmpData && cmpData.descriptor) {
|
|
14
|
+
const [appName, cmpName] = cmpData.descriptor.split(':');
|
|
15
|
+
if (appName !== myAppName) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let config;
|
|
19
|
+
const configArray: Object[] = [];
|
|
20
|
+
if ('configAsJson' in cmpData && cmpData.configAsJson && cmpData.configAsJson[myAppNameDashed]) {
|
|
21
|
+
config = cmpData.configAsJson[myAppNameDashed][cmpName];
|
|
22
|
+
if (config) {
|
|
23
|
+
configArray.push(config);
|
|
24
|
+
delete cmpData.configAsJson;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const sanitizedAppName = sanitizeGraphqlName(appName);
|
|
28
|
+
if ('config' in cmpData && cmpData.config && cmpData.config[sanitizedAppName]) {
|
|
29
|
+
config = cmpData.config[sanitizedAppName][sanitizeGraphqlName(cmpName)];
|
|
30
|
+
if (config) {
|
|
31
|
+
configArray.push(config);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (configArray.length) {
|
|
35
|
+
cmpData.config = Object.assign({}, ...configArray);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type {PageComponent} from '../types';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import {XP_COMPONENT_TYPE} from '../common/constants';
|
|
5
|
+
import {parseComponentPath} from './parseComponentPath';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export function restrictComponentsToPath(
|
|
9
|
+
contentType: string,
|
|
10
|
+
components?: PageComponent[],
|
|
11
|
+
componentPath?: string,
|
|
12
|
+
): PageComponent[] {
|
|
13
|
+
if (!componentPath || !components?.length) {
|
|
14
|
+
return components || [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// filter components to the requested one only
|
|
18
|
+
const component = components.find(cmp => {
|
|
19
|
+
return cmp.path === componentPath;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!component) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let result: PageComponent[] = [component];
|
|
27
|
+
if (component.type !== XP_COMPONENT_TYPE.LAYOUT) {
|
|
28
|
+
// remember to include all parent layouts too !
|
|
29
|
+
const cmpPath = parseComponentPath(contentType, component.path);
|
|
30
|
+
for (let i = cmpPath.length - 2; i >= 0; i--) {
|
|
31
|
+
const parentPath = cmpPath[i];
|
|
32
|
+
const parentCmp = components.find(cmp => cmp.path === `/${parentPath.region}/${parentPath.index}`);
|
|
33
|
+
if (parentCmp) {
|
|
34
|
+
result.unshift(parentCmp);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
// It's a layout, include child components
|
|
39
|
+
const childCmps = components.filter(cmp => cmp.path !== component.path && cmp.path.startsWith(component.path));
|
|
40
|
+
if (childCmps.length) {
|
|
41
|
+
result = result.concat(childCmps);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type {FetchContentResult} from '../types';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import {validateNotFound} from './validateNotFound';
|
|
5
|
+
import {validateShortcut} from './validateShortcut';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export function validateData(props: FetchContentResult): void {
|
|
9
|
+
validateNotFound(props);
|
|
10
|
+
validateShortcut(props);
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {FetchContentResult} from '../types';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import {notFound} from 'next/navigation';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export function validateNotFound(props: FetchContentResult): void {
|
|
8
|
+
const {error} = props;
|
|
9
|
+
if (error) {
|
|
10
|
+
switch (error.code) {
|
|
11
|
+
case '404':
|
|
12
|
+
console.warn(error.code, error.message);
|
|
13
|
+
notFound();
|
|
14
|
+
break;
|
|
15
|
+
default:
|
|
16
|
+
console.error(error.code, error.message);
|
|
17
|
+
throw new Error(error.message);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type {FetchContentResult} from '../types';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import {notFound, redirect, RedirectType} from 'next/navigation';
|
|
5
|
+
import {RENDER_MODE} from '../common/constants';
|
|
6
|
+
import {IS_DEV_MODE} from '../common/env';
|
|
7
|
+
import {UrlProcessor} from '../common/UrlProcessor';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function validateShortcut(props: FetchContentResult): void {
|
|
11
|
+
const {data, meta, error} = props;
|
|
12
|
+
const pageUrl = data?.get?.data?.target?.pageUrl;
|
|
13
|
+
if (meta.type === 'base:shortcut' && pageUrl) {
|
|
14
|
+
if (meta.renderMode !== RENDER_MODE.NEXT) {
|
|
15
|
+
// do not show shortcut targets in preview/edit mode
|
|
16
|
+
console.warn(404, `Shortcuts are not available in ${RENDER_MODE.NEXT} render mode`);
|
|
17
|
+
notFound();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const destination = UrlProcessor.process(pageUrl, meta, true);
|
|
21
|
+
console.debug(`Redirecting shortcut content [${meta.path}] to`, destination);
|
|
22
|
+
redirect(destination, RedirectType.replace);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// we can not set 418 for static paths,
|
|
26
|
+
// but we can show 404 instead to be handled in CS
|
|
27
|
+
const canNotRender = meta && !meta.canRender && meta.renderMode !== RENDER_MODE.EDIT;
|
|
28
|
+
|
|
29
|
+
const catchAllInNextProdMode = meta.renderMode === RENDER_MODE.NEXT && !IS_DEV_MODE && meta.catchAll;
|
|
30
|
+
|
|
31
|
+
const isNotFound = (error && error.code === '404') || canNotRender || catchAllInNextProdMode;
|
|
32
|
+
|
|
33
|
+
if (isNotFound) {
|
|
34
|
+
console.warn(404, `Can not render content at [${meta.path}]`);
|
|
35
|
+
notFound();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type {Dict, LocaleContextType} from '../types';
|
|
3
|
+
|
|
4
|
+
import React, {createContext, useContext, useEffect, useState} from 'react';
|
|
5
|
+
import {getPhrase, loadPhrases} from './i18n';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const LocaleContext = createContext<LocaleContextType>({
|
|
9
|
+
dictionary: {},
|
|
10
|
+
locale: '',
|
|
11
|
+
localize: (key: string, ...args: any[]) => key,
|
|
12
|
+
setLocale: (locale: string) => Promise.resolve({}),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const useLocaleContext = () => useContext(LocaleContext);
|
|
16
|
+
|
|
17
|
+
export const LocaleContextProvider = ({
|
|
18
|
+
children,
|
|
19
|
+
locale: localeProps = '',
|
|
20
|
+
}: {
|
|
21
|
+
children: any
|
|
22
|
+
locale?: string
|
|
23
|
+
}) => {
|
|
24
|
+
const [currentLocale, setLocaleState] = useState(localeProps);
|
|
25
|
+
const [dictionary, setPhrasesState] = useState<Dict>({});
|
|
26
|
+
|
|
27
|
+
const setLocale = async (locale: string): Promise<Dict> => {
|
|
28
|
+
setLocaleState(locale);
|
|
29
|
+
return loadPhrases(locale).then((phrases) => {
|
|
30
|
+
setPhrasesState(phrases);
|
|
31
|
+
console.info(`Loaded client-side phrases for locale "${locale}"`);
|
|
32
|
+
return phrases;
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (localeProps) {
|
|
38
|
+
void setLocale(localeProps); // void to avoid @typescript-eslint/no-floating-promises
|
|
39
|
+
}
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const localize = (key: string, ...args: any[]) => getPhrase(currentLocale, dictionary, key, ...args);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<LocaleContext.Provider value={{
|
|
46
|
+
setLocale,
|
|
47
|
+
locale: currentLocale,
|
|
48
|
+
dictionary,
|
|
49
|
+
localize,
|
|
50
|
+
}}>
|
|
51
|
+
{children}
|
|
52
|
+
</LocaleContext.Provider>
|
|
53
|
+
);
|
|
54
|
+
};
|
package/src/i18n/i18n.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type {Dict} from '../types';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export class I18n {
|
|
5
|
+
private static locale = '';
|
|
6
|
+
private static dictionary: Dict = {};
|
|
7
|
+
|
|
8
|
+
public static async setLocale(locale: string): Promise<Dict> {
|
|
9
|
+
this.locale = locale;
|
|
10
|
+
return loadPhrases(locale).then((phrases) => {
|
|
11
|
+
this.dictionary = phrases;
|
|
12
|
+
console.info(`Loaded server-side phrases for locale "${locale}"`);
|
|
13
|
+
return phrases;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public static localize(key: string, ...args: any[]): string {
|
|
18
|
+
return getPhrase(this.locale, this.dictionary, key, ...args);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public static getLocale() {
|
|
22
|
+
return this.locale;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public static getDictionary() {
|
|
26
|
+
return this.dictionary;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const loadPhrases = async (locale: string): Promise<Dict> => {
|
|
31
|
+
return import(`@phrases/${locale}.json`)
|
|
32
|
+
.then((module) => module.default)
|
|
33
|
+
.catch((e) => {
|
|
34
|
+
console.error(`Failed to load phrases for locale "${locale}"`, e);
|
|
35
|
+
return {};
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const getPhrase = (locale: string, dict: Dict, key: string, ...args: any[]) => {
|
|
40
|
+
const template = dict[key];
|
|
41
|
+
if (!template) {
|
|
42
|
+
console.warn(`Missing localization phrase for locale "${locale}": <${key}>`);
|
|
43
|
+
return `<${key}>`;
|
|
44
|
+
}
|
|
45
|
+
if (args.length) {
|
|
46
|
+
let index = -1;
|
|
47
|
+
return template.replace(/{(\d+)?}/g, (match, digit) => {
|
|
48
|
+
index++;
|
|
49
|
+
return args[digit || index] ?? match;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return template;
|
|
53
|
+
};
|
|
54
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export {
|
|
2
|
+
APP_NAME,
|
|
3
|
+
APP_NAME_DASHED,
|
|
4
|
+
APP_NAME_UNDERSCORED,
|
|
5
|
+
IS_DEV_MODE,
|
|
6
|
+
} from './common/env';
|
|
7
|
+
|
|
8
|
+
export type * from './types';
|
|
9
|
+
|
|
10
|
+
export {ComponentRegistry} from './common/ComponentRegistry';
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
CATCH_ALL,
|
|
14
|
+
JSESSIONID_HEADER,
|
|
15
|
+
PORTAL_COMPONENT_ATTRIBUTE,
|
|
16
|
+
PORTAL_REGION_ATTRIBUTE,
|
|
17
|
+
PROJECT_ID_HEADER,
|
|
18
|
+
RENDER_MODE,
|
|
19
|
+
RENDER_MODE_HEADER,
|
|
20
|
+
XP_BASE_URL_HEADER,
|
|
21
|
+
XP_COMPONENT_TYPE,
|
|
22
|
+
XP_REQUEST_TYPE,
|
|
23
|
+
} from './common/constants';
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
export {richTextQuery} from './guillotine/metadata/richTextQuery';
|
|
27
|
+
export {validateData} from './guillotine/validateData';
|
|
28
|
+
|
|
29
|
+
export {I18n} from './i18n/i18n';
|
|
30
|
+
|
|
31
|
+
export {getContentApiUrl} from './utils/getContentApiUrl';
|
|
32
|
+
export {getProjectLocaleConfig} from './utils/getProjectLocaleConfig';
|
|
33
|
+
export {getProjectLocaleConfigById} from './utils/getProjectLocaleConfigById';
|
|
34
|
+
export {getProjectLocaleConfigByLocale} from './utils/getProjectLocaleConfigByLocale';
|
|
35
|
+
export {getRequestLocaleInfo} from './utils/getRequestLocaleInfo';
|
|
36
|
+
export {sanitizeGraphqlName} from './utils/sanitizeGraphqlName';
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
UrlProcessor,
|
|
40
|
+
getAsset,
|
|
41
|
+
getUrl,
|
|
42
|
+
} from './common/UrlProcessor';
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export {fetchContent} from './guillotine/fetchContent';
|
|
2
|
+
export {fetchContentPathsForAllLocales} from './guillotine/fetchContentPathsForAllLocales';
|
|
3
|
+
export {fetchContentPathsForLocale} from './guillotine/fetchContentPathsForLocale';
|
|
4
|
+
export {fetchFromApi} from './guillotine/fetchFromApi';
|
|
5
|
+
export {fetchGuillotine} from './guillotine/fetchGuillotine';
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type {XP_COMPONENT_TYPE} from '../common/constants';
|
|
2
|
+
import type {Context} from './next';
|
|
3
|
+
|
|
4
|
+
// Differs from ComponentDefinition to avoid storing catchAll on all components
|
|
5
|
+
// catchAll is calculated in getType instead
|
|
6
|
+
export interface ComponentDefinitionParams {
|
|
7
|
+
query?: SelectedQueryMaybeVariablesFunc
|
|
8
|
+
configQuery?: string
|
|
9
|
+
processor?: DataProcessor
|
|
10
|
+
view?: React.FunctionComponent<any>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Object that configures the handling of a particular content type. All attributes are optional (see examples below), and missing values will fall back to default behavior:
|
|
15
|
+
* - 'query' (used in fetchContent.ts) Guillotine query for fetching content data, may also have a function that supplies guillotine variables. So, 'query' can EITHER be only a query string, OR also add a get-guillotine-variables function. In the latter case, 'query' can be an object with 'query' and 'variables' attributes, or an array where the query string is first and the get-variables function is second. Either way, the get-variables function takes two arguments: path (content path, mandatory) and context (next.js-supplied Context from getServerSideProps etc. Optional, and requires that fetchContent is called with the context, of course).
|
|
16
|
+
* - 'props' (used in fetchContent.ts) is a function for processing props after fetching them
|
|
17
|
+
* - 'view' (used in BasePage.tsx) is a React component: top-level content-type-specific rendering with the props first fetched from guillotine (and then optionally preprocessed with the function in 'props').
|
|
18
|
+
*/
|
|
19
|
+
export interface ComponentDefinition {
|
|
20
|
+
catchAll?: boolean; // will be set automatically depending on the binding
|
|
21
|
+
query?: SelectedQueryMaybeVariablesFunc,
|
|
22
|
+
configQuery?: string,
|
|
23
|
+
processor?: DataProcessor,
|
|
24
|
+
view?: React.FunctionComponent<any>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ComponentDescriptor {
|
|
28
|
+
type?: ComponentDefinition;
|
|
29
|
+
component?: PageComponent;
|
|
30
|
+
queryAndVariables?: QueryAndVariables;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ComponentDictionary = Record<string,ComponentDefinitionParams>;
|
|
34
|
+
|
|
35
|
+
// NB! Always return null or empty object from processor for next is unable to serialize undefined
|
|
36
|
+
export type DataProcessor = (data: any, context?: Context, config?: any) => Promise<Record<string, any>>;
|
|
37
|
+
|
|
38
|
+
export interface FragmentData {
|
|
39
|
+
id: string;
|
|
40
|
+
fragment: {
|
|
41
|
+
components: PageComponent[];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ImageData {
|
|
46
|
+
ref: string;
|
|
47
|
+
image: {
|
|
48
|
+
id: string,
|
|
49
|
+
} | null,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface LayoutData {
|
|
53
|
+
descriptor: string;
|
|
54
|
+
config?: any;
|
|
55
|
+
|
|
56
|
+
[customKeysFromQuery: string]: any;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface LinkData {
|
|
60
|
+
ref: string,
|
|
61
|
+
media: {
|
|
62
|
+
content: {
|
|
63
|
+
id: string,
|
|
64
|
+
}
|
|
65
|
+
} | null,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type MacroConfig = Record<string, any>;
|
|
69
|
+
|
|
70
|
+
export interface MacroData {
|
|
71
|
+
ref: string
|
|
72
|
+
name: string
|
|
73
|
+
descriptor: string
|
|
74
|
+
config: Record<string, MacroConfig>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface PageComponent {
|
|
78
|
+
[key: string]: any; // keeps ts happy when accessing component data field by XP_COMPONENT_TYPE type
|
|
79
|
+
type: XP_COMPONENT_TYPE;
|
|
80
|
+
path: string;
|
|
81
|
+
page?: PageData;
|
|
82
|
+
part?: PartData;
|
|
83
|
+
layout?: LayoutData;
|
|
84
|
+
fragment?: FragmentData;
|
|
85
|
+
text?: TextData;
|
|
86
|
+
image?: any;
|
|
87
|
+
regions?: RegionTree;
|
|
88
|
+
data?: any;
|
|
89
|
+
error?: any;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface PageData {
|
|
93
|
+
descriptor: string;
|
|
94
|
+
config?: any;
|
|
95
|
+
template?: string | null;
|
|
96
|
+
regions?: RegionTree;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface PageRegion {
|
|
100
|
+
name: string;
|
|
101
|
+
components: PageComponent[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface PartData {
|
|
105
|
+
descriptor: string;
|
|
106
|
+
config: any; // TODO NestedRecord?;
|
|
107
|
+
|
|
108
|
+
[customKeysFromQuery: string]: any;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface QueryAndVariables {
|
|
112
|
+
query: string
|
|
113
|
+
variables?: Record<string, any>
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type QueryGetter = (path: string, context?: Context, config?: any) => string;
|
|
117
|
+
|
|
118
|
+
export type SelectedQueryMaybeVariablesFunc =
|
|
119
|
+
| string
|
|
120
|
+
| QueryGetter
|
|
121
|
+
| {
|
|
122
|
+
query: string | QueryGetter
|
|
123
|
+
variables: VariablesGetter
|
|
124
|
+
}
|
|
125
|
+
| [string | QueryGetter, VariablesGetter];
|
|
126
|
+
|
|
127
|
+
export type RegionTree = Record<string, PageRegion>;
|
|
128
|
+
|
|
129
|
+
export interface RichTextData {
|
|
130
|
+
processedHtml: string
|
|
131
|
+
links: LinkData[]
|
|
132
|
+
macros: MacroData[]
|
|
133
|
+
images: ImageData[]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface TextData {
|
|
137
|
+
value: RichTextData;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// TODO: also access as arguments: dataAsJson, pageAsJson, configAsJson from the first (meta) call here?
|
|
141
|
+
// Another option could be to let the component or page controller pass those values to NextJS by a header
|
|
142
|
+
export type VariablesGetter = (path: string, context?: Context, config?: any) => VariablesGetterResult;
|
|
143
|
+
|
|
144
|
+
export interface VariablesGetterResult {
|
|
145
|
+
[variables: string]: any
|
|
146
|
+
path: string
|
|
147
|
+
}
|