@exdst-sitecore-content-sdk/astro 0.0.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 (87) hide show
  1. package/LICENSE.txt +202 -0
  2. package/README.md +3 -0
  3. package/package.json +101 -0
  4. package/src/client/index.ts +12 -0
  5. package/src/client/sitecore-astro-client.test.ts +271 -0
  6. package/src/client/sitecore-astro-client.ts +137 -0
  7. package/src/components/AstroImage.astro +114 -0
  8. package/src/components/Date.astro +76 -0
  9. package/src/components/DefaultEmptyFieldEditingComponentImage.astro +24 -0
  10. package/src/components/DefaultEmptyFieldEditingComponentText.astro +12 -0
  11. package/src/components/EditingScripts.astro +49 -0
  12. package/src/components/EmptyRendering.astro +3 -0
  13. package/src/components/ErrorBoundary.astro +77 -0
  14. package/src/components/FieldMetadata.astro +30 -0
  15. package/src/components/File.astro +46 -0
  16. package/src/components/HiddenRendering.astro +22 -0
  17. package/src/components/Image.astro +155 -0
  18. package/src/components/Link.astro +105 -0
  19. package/src/components/MissingComponent.astro +39 -0
  20. package/src/components/Placeholder/EmptyPlaceholder.astro +9 -0
  21. package/src/components/Placeholder/Placeholder.astro +100 -0
  22. package/src/components/Placeholder/PlaceholderMetadata.astro +102 -0
  23. package/src/components/Placeholder/PlaceholderUtils.astro +153 -0
  24. package/src/components/Placeholder/index.ts +5 -0
  25. package/src/components/Placeholder/models.ts +82 -0
  26. package/src/components/Placeholder/placeholder-utils.test.ts +162 -0
  27. package/src/components/Placeholder/placeholder-utils.ts +80 -0
  28. package/src/components/RenderWrapper.astro +31 -0
  29. package/src/components/RichText.astro +59 -0
  30. package/src/components/Text.astro +97 -0
  31. package/src/components/sharedTypes/index.ts +1 -0
  32. package/src/components/sharedTypes/props.ts +17 -0
  33. package/src/config/define-config.test.ts +526 -0
  34. package/src/config/define-config.ts +99 -0
  35. package/src/config/index.ts +1 -0
  36. package/src/config-cli/define-cli-config.test.ts +95 -0
  37. package/src/config-cli/define-cli-config.ts +50 -0
  38. package/src/config-cli/index.ts +1 -0
  39. package/src/context.ts +68 -0
  40. package/src/editing/constants.ts +8 -0
  41. package/src/editing/editing-config-middleware.test.ts +166 -0
  42. package/src/editing/editing-config-middleware.ts +111 -0
  43. package/src/editing/editing-render-middleware.test.ts +801 -0
  44. package/src/editing/editing-render-middleware.ts +288 -0
  45. package/src/editing/index.ts +16 -0
  46. package/src/editing/render-middleware.test.ts +57 -0
  47. package/src/editing/render-middleware.ts +51 -0
  48. package/src/editing/utils.test.ts +852 -0
  49. package/src/editing/utils.ts +308 -0
  50. package/src/enhancers/WithEmptyFieldEditingComponent.astro +56 -0
  51. package/src/enhancers/WithFieldMetadata.astro +31 -0
  52. package/src/env.d.ts +12 -0
  53. package/src/index.ts +16 -0
  54. package/src/middleware/index.ts +24 -0
  55. package/src/middleware/middleware.test.ts +507 -0
  56. package/src/middleware/middleware.ts +167 -0
  57. package/src/middleware/multisite-middleware.test.ts +672 -0
  58. package/src/middleware/multisite-middleware.ts +147 -0
  59. package/src/middleware/robots-middleware.test.ts +113 -0
  60. package/src/middleware/robots-middleware.ts +47 -0
  61. package/src/middleware/sitemap-middleware.test.ts +152 -0
  62. package/src/middleware/sitemap-middleware.ts +65 -0
  63. package/src/services/component-props-service.ts +182 -0
  64. package/src/sharedTypes/component-props.ts +17 -0
  65. package/src/site/index.ts +1 -0
  66. package/src/test-data/components/Bar.astro +0 -0
  67. package/src/test-data/components/Baz.astro +0 -0
  68. package/src/test-data/components/Foo.astro +0 -0
  69. package/src/test-data/components/Hero.variant.astro +0 -0
  70. package/src/test-data/components/NotComponent.bsx +0 -0
  71. package/src/test-data/components/Qux.astro +0 -0
  72. package/src/test-data/components/folded/Folded.astro +0 -0
  73. package/src/test-data/components/folded/random-file-2.docx +0 -0
  74. package/src/test-data/components/random-file.txt +0 -0
  75. package/src/test-data/helpers.ts +46 -0
  76. package/src/test-data/personalizeData.ts +63 -0
  77. package/src/tools/generate-map.ts +83 -0
  78. package/src/tools/index.ts +8 -0
  79. package/src/tools/templating/components.test.ts +305 -0
  80. package/src/tools/templating/components.ts +49 -0
  81. package/src/tools/templating/constants.ts +4 -0
  82. package/src/tools/templating/default-component.test.ts +31 -0
  83. package/src/tools/templating/default-component.ts +63 -0
  84. package/src/tools/templating/index.ts +2 -0
  85. package/src/utils/index.ts +1 -0
  86. package/src/utils/utils.test.ts +48 -0
  87. package/src/utils/utils.ts +52 -0
@@ -0,0 +1,308 @@
1
+ import {
2
+ DesignLibraryRenderPreviewData,
3
+ EDITING_ALLOWED_ORIGINS,
4
+ EditingRenderQueryParams,
5
+ isDesignLibraryMode,
6
+ LayoutKind,
7
+ PREVIEW_KEY,
8
+ QUERY_PARAM_EDITING_SECRET,
9
+ } from '@sitecore-content-sdk/core/editing';
10
+ import { DEFAULT_VARIANT } from '@sitecore-content-sdk/core/personalize';
11
+ import { SITE_KEY } from '@sitecore-content-sdk/core/site';
12
+ import {
13
+ EDITING_PASS_THROUGH_HEADERS,
14
+ QUERY_PARAM_VERCEL_PROTECTION_BYPASS,
15
+ QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE,
16
+ } from './constants';
17
+ import { IncomingHttpHeaders } from 'http';
18
+ import { NativeDataFetcher } from '@sitecore-content-sdk/core';
19
+ import { getAllowedOriginsFromEnv } from '@sitecore-content-sdk/core/utils';
20
+
21
+ /**
22
+ * Gets editing secret value from request
23
+ * @param {Request} req incoming request
24
+ * @returns {string | undefined} editing secret value if present
25
+ */
26
+ export const getEditingSecretFromRequest = (req: Request) => {
27
+ const reqUrl = (req as Request).url;
28
+
29
+ const url = new URL(reqUrl);
30
+ const secret = url.searchParams.get(QUERY_PARAM_EDITING_SECRET);
31
+
32
+ return secret;
33
+ };
34
+
35
+ /**
36
+ * Parses query string and its parameters to required editing parameters
37
+ * @param {URLSearchParams} query query string values
38
+ * @returns {EditingRenderQueryParams} editing parameters
39
+ */
40
+ export const mapEditingParams = (query: {
41
+ [key: string]: string | undefined;
42
+ }): { [key: string]: string | undefined } => {
43
+ const params = isDesignLibraryMode(query.mode)
44
+ ? {
45
+ itemId: query.sc_itemid,
46
+ componentUid: query.sc_uid,
47
+ renderingId: query.sc_renderingId,
48
+ language: query.sc_lang,
49
+ site: query.sc_site,
50
+ mode: query.mode,
51
+ dataSourceId: query.dataSourceId,
52
+ version: query.sc_version,
53
+ generation: query.generation,
54
+ }
55
+ : {
56
+ site: query.sc_site,
57
+ itemId: query.sc_itemid,
58
+ language: query.sc_lang,
59
+ // for sc_variantId we may employ multiple variants (page-layout + component level)
60
+ // they will be separated by commas (,)
61
+ variantIds: query.sc_variant || DEFAULT_VARIANT,
62
+ version: query.sc_version,
63
+ mode: query.mode,
64
+ layoutKind: query.sc_layoutKind,
65
+ };
66
+ return params;
67
+ };
68
+
69
+ /**
70
+ * Preview cookies enum
71
+ */
72
+ export enum PreviewCookies {
73
+ PREVIEW_DATA = '_preview_data',
74
+ }
75
+
76
+ /**
77
+ * Filters out preview cookies from a cookie string or array
78
+ * @param {string | string[] | null} cookies cookie header value
79
+ * @returns {string[] | null} filtered cookies
80
+ */
81
+ export const cleanupPreviewCookies = (cookies: string | string[] | null) => {
82
+ if (!cookies) {
83
+ return null;
84
+ }
85
+ if (!Array.isArray(cookies)) {
86
+ cookies = cookies.split(',');
87
+ }
88
+ // Filter out preview cookies
89
+ const filteredCookies = cookies.filter(
90
+ (cookie: string) => !new RegExp(`^${PreviewCookies.PREVIEW_DATA}=`).test(cookie)
91
+ );
92
+
93
+ return filteredCookies;
94
+ };
95
+
96
+ /**
97
+ * Gets the preview cookies to enable preview mode
98
+ * @param {string} site current site name
99
+ * @returns {string[]} list of cookies to set
100
+ */
101
+ export const getPreviewCookies = (site: string) => {
102
+ const previewSite = `${SITE_KEY}=${site}; Path=/; HttpOnly; SameSite=None; Secure`;
103
+ const previewCookie = `${PREVIEW_KEY}=true; Path=/; HttpOnly; SameSite=None; Secure`;
104
+ return [previewSite, previewCookie];
105
+ };
106
+
107
+ /**
108
+ * Returns the list of required query parameters based on the page editing mode
109
+ * @param {DesignLibraryMode | LayoutServicePageState.Preview | LayoutServicePageState.Edit} mode current page mode
110
+ * @returns {string[]} list of required parameters for validation
111
+ */
112
+ export const getRequiredEditingParamsList = (mode: EditingRenderQueryParams['mode']) => {
113
+ const editingRequiredParams = ['sc_site', 'sc_itemid', 'sc_lang', 'route', 'mode'];
114
+
115
+ const componentRequiredParams = [
116
+ 'sc_site',
117
+ 'sc_itemid',
118
+ 'sc_renderingId',
119
+ 'sc_uid',
120
+ 'sc_lang',
121
+ 'mode',
122
+ ];
123
+ return isDesignLibraryMode(mode) ? componentRequiredParams : editingRequiredParams;
124
+ };
125
+
126
+ /**
127
+ * Gets query parameters that should be passed along to subsequent requests (e.g. for deployment protection bypass)
128
+ * @param {object} query URLSearchParams object from incoming URL
129
+ * @returns object of approved query parameters
130
+ */
131
+ export const getQueryParamsForPropagation = (
132
+ query: Partial<{ [key: string]: string | string[] }>
133
+ ): { [key: string]: string } => {
134
+ const params: { [key: string]: string } = {};
135
+ if (query[QUERY_PARAM_VERCEL_PROTECTION_BYPASS]) {
136
+ params[QUERY_PARAM_VERCEL_PROTECTION_BYPASS] = query[
137
+ QUERY_PARAM_VERCEL_PROTECTION_BYPASS
138
+ ] as string;
139
+ }
140
+ if (query[QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE]) {
141
+ params[QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE] = query[
142
+ QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE
143
+ ] as string;
144
+ }
145
+ return params;
146
+ };
147
+
148
+ /**
149
+ * Get headers that should be passed along to subsequent requests
150
+ * @param {IncomingHttpHeaders | Headers} headers Incoming HTTP Headers
151
+ * @returns Object of approved headers
152
+ */
153
+ export const getHeadersForPropagation = (
154
+ headers: IncomingHttpHeaders | Headers
155
+ ): { [key: string]: string } => {
156
+ // Filter and normalize headers
157
+ const filteredHeaders = EDITING_PASS_THROUGH_HEADERS.reduce((acc, header) => {
158
+ const value = (headers as Headers).get
159
+ ? (headers as Headers).get(header)
160
+ : (headers as IncomingHttpHeaders)[header];
161
+ if (value) {
162
+ acc[header] = Array.isArray(value) ? value.join(', ') : value;
163
+ }
164
+ return acc;
165
+ }, {} as Record<string, string>);
166
+
167
+ return filteredHeaders;
168
+ };
169
+
170
+ /**
171
+ * Performs an internal request to get the HTML for the editing mode
172
+ * @param {string} requestUrl URL to send request to
173
+ * @param {object} propagatedQsParams query string params to use with request
174
+ * @param {object} propagatedHeaders headers to use with request
175
+ * @param {string[]} cookies cookies to use with request
176
+ * @param {NativeDataFetcher} dataFetcher NativeFetcher instance to send request with
177
+ * @returns {string} HTML with editing markup
178
+ */
179
+ export const getEditingRequestHtml = async (
180
+ requestUrl: URL,
181
+ propagatedQsParams: { [key: string]: string | undefined },
182
+ propagatedHeaders: { [key: string]: string },
183
+ cookies: string[],
184
+ dataFetcher: NativeDataFetcher
185
+ ): Promise<string> => {
186
+ // Grab the preview cookies to send on to the render request
187
+ propagatedHeaders.cookie = `${
188
+ propagatedHeaders.cookie ? propagatedHeaders.cookie + ';' : ''
189
+ }${cookies.join(';')}`;
190
+ // enable content sdk preview
191
+ propagatedHeaders.__content_sdk_preview = '1';
192
+
193
+ for (const key in propagatedQsParams) {
194
+ if ({}.hasOwnProperty.call(propagatedQsParams, key)) {
195
+ propagatedQsParams[key] && requestUrl.searchParams.append(key, propagatedQsParams[key]);
196
+ }
197
+ }
198
+ requestUrl.searchParams.append('timestamp', Date.now().toString());
199
+
200
+ const pageRes = await dataFetcher
201
+ .get<string>(requestUrl.toString(), {
202
+ credentials: 'include',
203
+ headers: propagatedHeaders,
204
+ })
205
+ .catch((err) => {
206
+ // We need to handle not found error provided by Vercel
207
+ // for `fallback: false` pages
208
+ if (err.response.status === 404) {
209
+ return err.response;
210
+ }
211
+
212
+ throw err;
213
+ });
214
+
215
+ let html = pageRes.data;
216
+ if (!html || html.length === 0) {
217
+ throw new Error(`Failed to render html for ${requestUrl.toString()}`);
218
+ }
219
+
220
+ // replace phkey attribute with key attribute so that newly added renderings
221
+ // show correct placeholders, so save and refresh won't be needed after adding each rendering
222
+ html = html.replace(new RegExp('phkey', 'g'), 'key');
223
+
224
+ return html;
225
+ };
226
+
227
+ /**
228
+ * Type guard for Design Library mode
229
+ * @param {object} data preview data to check
230
+ * @returns true if the data is EditingPreviewData
231
+ * @see EditingPreviewData
232
+ */
233
+ export const isDesignLibraryPreviewData = (
234
+ data: unknown
235
+ ): data is DesignLibraryRenderPreviewData => {
236
+ return (
237
+ typeof data === 'object' &&
238
+ data !== null &&
239
+ 'mode' in data &&
240
+ isDesignLibraryMode((data as DesignLibraryRenderPreviewData).mode)
241
+ );
242
+ };
243
+
244
+ /**
245
+ * Server URL Resolution order (highest to lowest priority):
246
+ * 1. `config.sitecoreInternalEditingHostUrl` (explicitly set in config)
247
+ * 2. Environment variable `SITECORE_INTERNAL_EDITING_HOST_URL`
248
+ * 3. Fallbacks:
249
+ * - For XM Cloud deployments → `'http://localhost:3000'`
250
+ * - For all other cases → use the request `Host` header
251
+ * Note we use https protocol on Vercel due to serverless function architecture.
252
+ * In all other scenarios, including localhost (with or without a proxy e.g. ngrok)
253
+ * and within a nodejs container, http protocol should be used.
254
+ *
255
+ * For information about the VERCEL environment variable, see
256
+ * https://vercel.com/docs/environment-variables#system-environment-variables
257
+ * @param {Request} req
258
+ */
259
+ export const resolveServerUrl = (req: Request) => {
260
+ const internalHostUrl = process.env.SITECORE_INTERNAL_EDITING_HOST_URL;
261
+ if (internalHostUrl) {
262
+ return internalHostUrl;
263
+ }
264
+
265
+ // in xmc deployment we always use localhost:3000
266
+ if (process.env.SITECORE) {
267
+ return 'http://localhost:3000';
268
+ }
269
+
270
+ // to preserve auth headers, use https if we're in our 3 main hosting options
271
+ const useHttps = (process.env.VERCEL || process.env.NETLIFY) !== undefined;
272
+ // use https for requests with auth but also support unsecured http rendering hosts
273
+ return `${useHttps ? 'https' : 'http'}://${req.headers.get('host') ?? undefined}`;
274
+ };
275
+
276
+ /**
277
+ * Gets the Content-Security-Policy header value
278
+ * @returns Content-Security-Policy header value
279
+ */
280
+ export const getCSPHeader = () => {
281
+ return `frame-ancestors 'self' ${[...getAllowedOriginsFromEnv(), ...EDITING_ALLOWED_ORIGINS].join(
282
+ ' '
283
+ )}`;
284
+ };
285
+
286
+ /**
287
+ * Gets the object with query params from URLSearchParams object
288
+ * @param {URLSearchParams} query - URLSearchParams query object
289
+ * @returns object with query params
290
+ */
291
+ export const getEditingRenderQueryParams = (query: URLSearchParams): EditingRenderQueryParams => {
292
+ const params = Object.fromEntries(query.entries());
293
+
294
+ return {
295
+ ...params,
296
+ secret: params.secret ?? '',
297
+ sc_lang: params.sc_lang ?? '',
298
+ sc_itemid: params.sc_itemid ?? '',
299
+ sc_site: params.sc_site ?? '',
300
+ route: params.route ?? '',
301
+ mode: params.mode as EditingRenderQueryParams['mode'],
302
+ sc_layoutKind: params.sc_layoutkind as LayoutKind,
303
+ sc_variant: params.sc_variant ?? undefined,
304
+ sc_version: params.sc_version ?? undefined,
305
+ sc_renderingId: params.sc_renderingid ?? undefined,
306
+ dataSourceId: params.datasourceid ?? undefined,
307
+ };
308
+ };
@@ -0,0 +1,56 @@
1
+ ---
2
+ /**
3
+ * Returns the passed field component or default component in case field value is empty and edit mode is 'metadata'
4
+ */
5
+
6
+ import {
7
+ GenericFieldValue,
8
+ Field,
9
+ isFieldValueEmpty,
10
+ FieldMetadata,
11
+ } from '@sitecore-content-sdk/core/layout';
12
+ import { AstroContentSdkComponent } from '../sharedTypes/component-props';
13
+
14
+ interface FieldComponentProps {
15
+ // Partial<T> type is used here because _field.value_ could be required or optional for the different field types
16
+ field?: (Partial<Field> | GenericFieldValue) & FieldMetadata;
17
+ editable?: boolean;
18
+ emptyFieldEditingComponent?: AstroContentSdkComponent;
19
+ defaultEmptyFieldEditingComponent: AstroContentSdkComponent;
20
+ }
21
+
22
+ const getEmptyFieldEditingComponent = (
23
+ props: FieldComponentProps
24
+ ): AstroContentSdkComponent | null => {
25
+ const { editable = true } = props;
26
+ if (props.field?.metadata && editable && isFieldValueEmpty(props.field)) {
27
+ return props.emptyFieldEditingComponent || props.defaultEmptyFieldEditingComponent;
28
+ }
29
+
30
+ return null;
31
+ };
32
+
33
+ const props = Astro.props as FieldComponentProps;
34
+ const { editable = true } = props;
35
+
36
+ const isEmptyEditableField = props.field?.metadata && editable && isFieldValueEmpty(props.field);
37
+
38
+ const EmptyComponent = getEmptyFieldEditingComponent(props);
39
+
40
+ let { defaultEmptyFieldEditingComponent, emptyFieldEditingComponent, ...resolvedProps } = props;
41
+
42
+ // If no custom empty field editing component is provided, we can omit unnecessary props
43
+ // to do not insert them to html
44
+ if (!props.emptyFieldEditingComponent) {
45
+ resolvedProps = {
46
+ ...resolvedProps,
47
+ editable: undefined,
48
+ field: undefined,
49
+ };
50
+ }
51
+ ---
52
+
53
+ <>
54
+ {isEmptyEditableField && EmptyComponent && <EmptyComponent {...resolvedProps} />}
55
+ {!isEmptyEditableField && <slot />}
56
+ </>
@@ -0,0 +1,31 @@
1
+ ---
2
+ /**
3
+ * Wraps the field component with metadata markup intended to be used for chromes hydration in Pages
4
+ */
5
+
6
+ import FieldMetadata from '../components/FieldMetadata.astro';
7
+
8
+ interface WithMetadataProps {
9
+ field?: {
10
+ metadata?: { [key: string]: unknown };
11
+ };
12
+ editable?: boolean;
13
+ }
14
+
15
+ const componentProps = Astro.props as WithMetadataProps;
16
+
17
+ const { editable = true } = componentProps;
18
+ const metadata = componentProps.field?.metadata;
19
+ const isEditable = metadata && editable;
20
+ ---
21
+
22
+ <>
23
+ {!isEditable && <slot />}
24
+ {
25
+ isEditable && (
26
+ <FieldMetadata metadata={metadata}>
27
+ <slot />
28
+ </FieldMetadata>
29
+ )
30
+ }
31
+ </>
package/src/env.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /* eslint-disable spaced-comment */
2
+ /* eslint-disable @typescript-eslint/triple-slash-reference */
3
+ /// <reference types="astro/client" />
4
+ declare global {
5
+ namespace App {
6
+ interface Locals extends Record<string, any> {
7
+ skipMiddleware?: boolean;
8
+ }
9
+ }
10
+ }
11
+
12
+ export {};
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export { HTMLLink } from '@sitecore-content-sdk/core';
2
+
3
+ export {
4
+ Field,
5
+ LayoutServiceData,
6
+ ComponentRendering,
7
+ ComponentFields,
8
+ ComponentParams,
9
+ } from '@sitecore-content-sdk/core/layout';
10
+
11
+ export {
12
+ AstroContentSdkComponent,
13
+ ComponentMap,
14
+ } from './sharedTypes/component-props';
15
+
16
+ export { PageMode, ErrorPage, Page } from '@sitecore-content-sdk/core/client';
@@ -0,0 +1,24 @@
1
+ export { debug } from '@sitecore-content-sdk/core';
2
+ export {
3
+ MiddlewareBase,
4
+ MiddlewareBaseConfig,
5
+ Middleware,
6
+ } from './middleware';
7
+ export { RobotsMiddleware } from './robots-middleware';
8
+ export { SitemapMiddleware } from './sitemap-middleware';
9
+ export {
10
+ PersonalizeService,
11
+ PersonalizeServiceConfig,
12
+ } from '@sitecore-content-sdk/core/personalize';
13
+ export {
14
+ MultisiteMiddleware,
15
+ MultisiteMiddlewareConfig,
16
+ } from './multisite-middleware';
17
+ export {
18
+ RedirectsService,
19
+ RedirectsServiceConfig,
20
+ REDIRECT_TYPE_301,
21
+ REDIRECT_TYPE_302,
22
+ REDIRECT_TYPE_SERVER_TRANSFER,
23
+ RedirectInfo,
24
+ } from '@sitecore-content-sdk/core/site';