@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,288 @@
1
+ import { debug, NativeDataFetcher } from '@sitecore-content-sdk/core';
2
+ import {
3
+ QUERY_PARAM_EDITING_SECRET,
4
+ EDITING_ALLOWED_ORIGINS,
5
+ } from '@sitecore-content-sdk/core/editing';
6
+ import { getEditingSecret } from '../utils';
7
+ import { getEnforcedCorsHeaders } from '@sitecore-content-sdk/core/utils';
8
+ import { LayoutServicePageState } from '@sitecore-content-sdk/core/layout';
9
+ import { RenderMiddlewareBase } from './render-middleware';
10
+ import {
11
+ cleanupPreviewCookies,
12
+ getCSPHeader,
13
+ getEditingRenderQueryParams,
14
+ getEditingRequestHtml,
15
+ getHeadersForPropagation,
16
+ getPreviewCookies,
17
+ getQueryParamsForPropagation,
18
+ getRequiredEditingParamsList,
19
+ mapEditingParams,
20
+ PreviewCookies,
21
+ resolveServerUrl,
22
+ } from './utils';
23
+ import * as cookie from 'cookie';
24
+
25
+ /**
26
+ * Configuration for the Editing Render Middleware.
27
+ */
28
+ export type EditingRenderMiddlewareConfig = {
29
+ /**
30
+ * Function used to determine route/page URL to render.
31
+ * This may be necessary for certain custom routing configurations.
32
+ * @param {string} itemPath The Sitecore relative item path e.g. '/styleguide'
33
+ * @returns {string} The URL to render
34
+ * @default `${itemPath}`
35
+ */
36
+ resolvePageUrl?: (itemPath: string) => string;
37
+ /**
38
+ * The internal host URL for the application, used for server-side requests for page rendering during editing.
39
+ */
40
+ sitecoreInternalEditingHostUrl?: string;
41
+ };
42
+
43
+ /**
44
+ * Middleware / handler for use in the editing render API route (e.g. '/api/editing/render')
45
+ * which is required for Sitecore editing support.
46
+ */
47
+ export class EditingRenderMiddleware extends RenderMiddlewareBase {
48
+ private dataFetcher: NativeDataFetcher;
49
+ /**
50
+ * @param {EditingRenderMiddlewareConfig} [config] Editing render middleware config
51
+ */
52
+ constructor(public config?: EditingRenderMiddlewareConfig) {
53
+ super();
54
+ this.dataFetcher = new NativeDataFetcher({ debugger: debug.editing });
55
+ }
56
+
57
+ /**
58
+ * Gets the API route handler
59
+ * @returns route handler
60
+ */
61
+ public getHandler(): (req: Request) => Promise<Response> {
62
+ return this.handler;
63
+ }
64
+
65
+ /**
66
+ * Gets the preview data cookies string
67
+ * @param {object} data preview data
68
+ * @returns Cookie string with the preview data
69
+ */
70
+ private getPreviewDataCookies = (data: {
71
+ [key: string]: string | string[] | undefined;
72
+ }): string => {
73
+ return cookie.serialize(
74
+ PreviewCookies.PREVIEW_DATA,
75
+ JSON.stringify(data, (_, value) => (value === null ? undefined : value)),
76
+ {
77
+ httpOnly: true,
78
+ path: '/',
79
+ maxAge: 3,
80
+ sameSite: 'none',
81
+ secure: true,
82
+ }
83
+ );
84
+ };
85
+
86
+ private handler = async (_req: Request): Promise<Response> => {
87
+ const { method, headers } = _req;
88
+ const url = new URL(_req.url.toLowerCase());
89
+ const query = getEditingRenderQueryParams(url.searchParams);
90
+
91
+ debug.editing('editing render middleware start: %o', {
92
+ method,
93
+ query,
94
+ headers,
95
+ });
96
+
97
+ const _res = new Response();
98
+ _res.headers.append('Content-Type', 'application/json; charset=utf-8');
99
+
100
+ const corsHeaders = getEnforcedCorsHeaders({
101
+ requestMethod: _req.method,
102
+ headers: _req.headers,
103
+ allowedOrigins: EDITING_ALLOWED_ORIGINS,
104
+ });
105
+
106
+ if (!corsHeaders) {
107
+ debug.editing(
108
+ 'invalid origin host - set allowed origins in JSS_ALLOWED_ORIGINS environment variable'
109
+ );
110
+
111
+ return new Response(
112
+ JSON.stringify({
113
+ html: `<html><body>Requests from origin ${_req.headers?.get(
114
+ 'origin'
115
+ )} not allowed</body></html>`,
116
+ }),
117
+ {
118
+ status: 401,
119
+ headers: _res.headers,
120
+ }
121
+ );
122
+ }
123
+
124
+ Object.keys(corsHeaders).forEach((key) => {
125
+ _res.headers.append(key, corsHeaders[key]);
126
+ });
127
+
128
+ // Validate secret
129
+ const secret = query[QUERY_PARAM_EDITING_SECRET];
130
+ if (secret !== getEditingSecret()) {
131
+ debug.editing('invalid editing secret - sent "%s" expected "%s"', secret, getEditingSecret());
132
+
133
+ return new Response(
134
+ JSON.stringify({
135
+ html: '<html><body>Missing or invalid secret</body></html>',
136
+ }),
137
+ {
138
+ status: 401,
139
+ headers: _res.headers,
140
+ }
141
+ );
142
+ }
143
+
144
+ if (_req.method === 'OPTIONS') {
145
+ debug.editing('preflight request');
146
+
147
+ // CORS headers are set by enforceCors
148
+ return new Response(null, {
149
+ status: 204,
150
+ headers: _res.headers,
151
+ });
152
+ }
153
+
154
+ if (_req.method !== 'GET') {
155
+ debug.editing('invalid method - sent %s expected GET', _req.method);
156
+
157
+ _res.headers.append('Allow', 'GET');
158
+
159
+ return new Response(
160
+ JSON.stringify({
161
+ html: `<html><body>Invalid request method '${_req.method}'</body></html>`,
162
+ }),
163
+ {
164
+ status: 405,
165
+ headers: _res.headers,
166
+ }
167
+ );
168
+ }
169
+
170
+ const startTimestamp = Date.now();
171
+
172
+ const mode = query.mode;
173
+
174
+ const requiredQueryParams = getRequiredEditingParamsList(mode);
175
+
176
+ const missingQueryParams = requiredQueryParams.filter((param) => !query[param]);
177
+
178
+ // Validate query parameters
179
+ if (missingQueryParams.length) {
180
+ debug.editing('missing required query parameters: %o', missingQueryParams);
181
+
182
+ return new Response(
183
+ JSON.stringify({
184
+ html: `<html><body>Missing required query parameters: ${missingQueryParams.join(
185
+ ', '
186
+ )}</body></html>`,
187
+ }),
188
+ {
189
+ status: 400,
190
+ headers: _res.headers,
191
+ }
192
+ );
193
+ }
194
+
195
+ const previewDataParams = mapEditingParams(query as { [key: string]: string });
196
+
197
+ const previewDataCookies = this.getPreviewDataCookies({
198
+ ...previewDataParams,
199
+ variantIds: previewDataParams.variantIds?.split(','),
200
+ });
201
+ _res.headers.append('Set-Cookie', previewDataCookies);
202
+
203
+ // Set Preview mode identifier cookie, if the page is rendered in Sitecore Preview mode
204
+ if (mode === LayoutServicePageState.Preview) {
205
+ const previewCookies = getPreviewCookies(query.sc_site as string);
206
+
207
+ previewCookies.forEach((cookie) => {
208
+ _res.headers.append('Set-Cookie', cookie);
209
+ });
210
+ }
211
+
212
+ // Restrict the page to be rendered only within the allowed origins
213
+ _res.headers.append('Content-Security-Policy', getCSPHeader());
214
+
215
+ const encodedRoute = encodeURI(query.route);
216
+ const route = this.config?.resolvePageUrl?.(encodedRoute) || encodedRoute;
217
+
218
+ const base = this.config?.sitecoreInternalEditingHostUrl || resolveServerUrl(_req);
219
+ const requestUrl = new URL(route, base);
220
+
221
+ // Grab the preview cookies to send on to the render request
222
+ const cookies = _res.headers.getSetCookie();
223
+
224
+ // Make actual render request for page route, passing on preview cookies as well as any approved query string parameters.
225
+ // Note timestamp effectively disables caching the request (no amount of cache headers seemed to do it)
226
+ try {
227
+ debug.editing('fetching page route for %s', query.route);
228
+
229
+ // Get query string parameters to propagate on subsequent requests (e.g. for deployment protection bypass)
230
+ const propagatedQsParams = getQueryParamsForPropagation(query as { [key: string]: string });
231
+
232
+ // Get headers to propagate on subsequent requests
233
+ const propagatedHeaders = getHeadersForPropagation(headers);
234
+
235
+ const html = await getEditingRequestHtml(
236
+ requestUrl,
237
+ propagatedQsParams,
238
+ propagatedHeaders,
239
+ cookies,
240
+ this.dataFetcher
241
+ );
242
+
243
+ // remove preview cookies to not leak them to the browser
244
+ if (cookies && Array.isArray(cookies)) {
245
+ const filteredCookies = cleanupPreviewCookies(cookies);
246
+ _res.headers.delete('Set-Cookie');
247
+ filteredCookies?.forEach((cookie) => {
248
+ _res.headers.append('Set-Cookie', cookie);
249
+ });
250
+ }
251
+
252
+ debug.editing('editing render middleware end in %dms: %o', Date.now() - startTimestamp, {
253
+ status: 200,
254
+ route,
255
+ });
256
+
257
+ _res.headers.set('Content-Type', 'text/html; charset=utf-8');
258
+
259
+ return new Response(html, {
260
+ status: 200,
261
+ headers: _res.headers,
262
+ });
263
+ } catch (err) {
264
+ const error = err as Record<string, unknown>;
265
+
266
+ console.error(error);
267
+
268
+ if (error.response) {
269
+ console.info(
270
+ // eslint-disable-next-line quotes
271
+ "Hint: for non-standard server or Next.js route configurations, you may need to override 'resolvePageUrl' or set the 'sitecoreInternalEditingHostUrl' (or SITECORE_INTERNAL_EDITING_HOST_URL env variable) available on the 'EditingRenderMiddleware' config."
272
+ );
273
+ }
274
+
275
+ // remove preview cookies to not leak them to the browser
276
+ const filteredCookies = cleanupPreviewCookies(cookies);
277
+ _res.headers.delete('Set-Cookie');
278
+ filteredCookies?.forEach((cookie) => {
279
+ _res.headers.append('Set-Cookie', cookie);
280
+ });
281
+
282
+ return new Response(`<html><body>${error}</body></html>`, {
283
+ status: 500,
284
+ headers: _res.headers,
285
+ });
286
+ }
287
+ };
288
+ }
@@ -0,0 +1,16 @@
1
+ export {
2
+ EditingConfigMiddleware,
3
+ EditingConfigMiddlewareConfig,
4
+ } from './editing-config-middleware';
5
+
6
+ export {
7
+ EditingRenderMiddleware,
8
+ EditingRenderMiddlewareConfig,
9
+ } from './editing-render-middleware';
10
+
11
+ export {
12
+ isDesignLibraryPreviewData,
13
+ getQueryParamsForPropagation,
14
+ getHeadersForPropagation,
15
+ PreviewCookies,
16
+ } from './utils';
@@ -0,0 +1,57 @@
1
+ /* eslint-disable dot-notation */
2
+ import chai from 'chai';
3
+ import chaiString from 'chai-string';
4
+ import { QUERY_PARAM_EDITING_SECRET } from '@sitecore-content-sdk/core/editing';
5
+ import { RenderMiddlewareBase } from './render-middleware';
6
+ import {
7
+ QUERY_PARAM_VERCEL_PROTECTION_BYPASS,
8
+ QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE,
9
+ EDITING_PASS_THROUGH_HEADERS,
10
+ } from './constants';
11
+
12
+ const expect = chai.use(chaiString).expect;
13
+
14
+ describe('RenderMiddlewareBase', () => {
15
+ class SampleMiddleware extends RenderMiddlewareBase {}
16
+
17
+ describe('getQueryParamsForPropagation', () => {
18
+ it('should construct query params for protection bypass', () => {
19
+ const middleware = new SampleMiddleware();
20
+
21
+ const secret = 'secret1234';
22
+ const vercelBypassToken = 'token1234Vercel';
23
+ const vercelBypassCookie = 'samesitenone';
24
+ const query = new URLSearchParams({
25
+ [QUERY_PARAM_EDITING_SECRET]: secret,
26
+ [QUERY_PARAM_VERCEL_PROTECTION_BYPASS]: vercelBypassToken,
27
+ [QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE]: vercelBypassCookie,
28
+ });
29
+
30
+ const approvedQuery = new URLSearchParams({
31
+ [QUERY_PARAM_VERCEL_PROTECTION_BYPASS]: vercelBypassToken,
32
+ [QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE]: vercelBypassCookie,
33
+ });
34
+
35
+ expect(middleware['getQueryParamsForPropagation'](query)).to.deep.equal(approvedQuery);
36
+ });
37
+ });
38
+
39
+ describe('getHeadersForPropagation', () => {
40
+ it('should return approved headers', () => {
41
+ const middleware = new SampleMiddleware();
42
+
43
+ const allHeaders = new Headers();
44
+ const approvedHeaders = new Headers();
45
+
46
+ EDITING_PASS_THROUGH_HEADERS.forEach((key) => {
47
+ allHeaders.append(key, `${key}-value`);
48
+ approvedHeaders.append(key, `${key}-value`);
49
+ });
50
+
51
+ allHeaders.append('nope', 'nope');
52
+ allHeaders.append('should-not-pass', 'n/a');
53
+
54
+ expect(middleware['getHeadersForPropagation'](allHeaders)).to.deep.equal(approvedHeaders);
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,51 @@
1
+ import {
2
+ QUERY_PARAM_VERCEL_PROTECTION_BYPASS,
3
+ QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE,
4
+ EDITING_PASS_THROUGH_HEADERS,
5
+ } from './constants';
6
+
7
+ /**
8
+ * Base class for middleware that handles pages and components rendering in Sitecore Editors.
9
+ * @deprecated getQueryParamsForPropagation and getHeadersForPropagation methods have been moved to separate exports
10
+ */
11
+ export abstract class RenderMiddlewareBase {
12
+ /**
13
+ * Gets query parameters that should be passed along to subsequent requests (e.g. for deployment protection bypass)
14
+ * @param {object} query URLSearchParams object from incoming URL
15
+ * @returns URLSearchParams object of approved query parameters
16
+ */
17
+ protected getQueryParamsForPropagation = (query: URLSearchParams): URLSearchParams => {
18
+ const params = new URLSearchParams();
19
+ if (query.get(QUERY_PARAM_VERCEL_PROTECTION_BYPASS)) {
20
+ params.append(
21
+ QUERY_PARAM_VERCEL_PROTECTION_BYPASS,
22
+ query.get(QUERY_PARAM_VERCEL_PROTECTION_BYPASS) as string
23
+ );
24
+ }
25
+ if (query.get(QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE)) {
26
+ params.append(
27
+ QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE,
28
+ query.get(QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE) as string
29
+ );
30
+ }
31
+ return params;
32
+ };
33
+
34
+ /**
35
+ * Get headers that should be passed along to subsequent requests
36
+ * @param {IncomingHttpHeaders} headers Incoming HTTP Headers
37
+ * @returns Object of approved headers
38
+ */
39
+ protected getHeadersForPropagation = (headers: Headers): Headers => {
40
+ // Filter and normalize headers
41
+ const filteredHeaders = EDITING_PASS_THROUGH_HEADERS.reduce((acc, header) => {
42
+ const value = headers.get(header);
43
+ if (value) {
44
+ acc.append(header, Array.isArray(value) ? value.join(', ') : value);
45
+ }
46
+ return acc;
47
+ }, new Headers());
48
+
49
+ return filteredHeaders;
50
+ };
51
+ }