@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.
- package/LICENSE.txt +202 -0
- package/README.md +3 -0
- package/package.json +101 -0
- package/src/client/index.ts +12 -0
- package/src/client/sitecore-astro-client.test.ts +271 -0
- package/src/client/sitecore-astro-client.ts +137 -0
- package/src/components/AstroImage.astro +114 -0
- package/src/components/Date.astro +76 -0
- package/src/components/DefaultEmptyFieldEditingComponentImage.astro +24 -0
- package/src/components/DefaultEmptyFieldEditingComponentText.astro +12 -0
- package/src/components/EditingScripts.astro +49 -0
- package/src/components/EmptyRendering.astro +3 -0
- package/src/components/ErrorBoundary.astro +77 -0
- package/src/components/FieldMetadata.astro +30 -0
- package/src/components/File.astro +46 -0
- package/src/components/HiddenRendering.astro +22 -0
- package/src/components/Image.astro +155 -0
- package/src/components/Link.astro +105 -0
- package/src/components/MissingComponent.astro +39 -0
- package/src/components/Placeholder/EmptyPlaceholder.astro +9 -0
- package/src/components/Placeholder/Placeholder.astro +100 -0
- package/src/components/Placeholder/PlaceholderMetadata.astro +102 -0
- package/src/components/Placeholder/PlaceholderUtils.astro +153 -0
- package/src/components/Placeholder/index.ts +5 -0
- package/src/components/Placeholder/models.ts +82 -0
- package/src/components/Placeholder/placeholder-utils.test.ts +162 -0
- package/src/components/Placeholder/placeholder-utils.ts +80 -0
- package/src/components/RenderWrapper.astro +31 -0
- package/src/components/RichText.astro +59 -0
- package/src/components/Text.astro +97 -0
- package/src/components/sharedTypes/index.ts +1 -0
- package/src/components/sharedTypes/props.ts +17 -0
- package/src/config/define-config.test.ts +526 -0
- package/src/config/define-config.ts +99 -0
- package/src/config/index.ts +1 -0
- package/src/config-cli/define-cli-config.test.ts +95 -0
- package/src/config-cli/define-cli-config.ts +50 -0
- package/src/config-cli/index.ts +1 -0
- package/src/context.ts +68 -0
- package/src/editing/constants.ts +8 -0
- package/src/editing/editing-config-middleware.test.ts +166 -0
- package/src/editing/editing-config-middleware.ts +111 -0
- package/src/editing/editing-render-middleware.test.ts +801 -0
- package/src/editing/editing-render-middleware.ts +288 -0
- package/src/editing/index.ts +16 -0
- package/src/editing/render-middleware.test.ts +57 -0
- package/src/editing/render-middleware.ts +51 -0
- package/src/editing/utils.test.ts +852 -0
- package/src/editing/utils.ts +308 -0
- package/src/enhancers/WithEmptyFieldEditingComponent.astro +56 -0
- package/src/enhancers/WithFieldMetadata.astro +31 -0
- package/src/env.d.ts +12 -0
- package/src/index.ts +16 -0
- package/src/middleware/index.ts +24 -0
- package/src/middleware/middleware.test.ts +507 -0
- package/src/middleware/middleware.ts +167 -0
- package/src/middleware/multisite-middleware.test.ts +672 -0
- package/src/middleware/multisite-middleware.ts +147 -0
- package/src/middleware/robots-middleware.test.ts +113 -0
- package/src/middleware/robots-middleware.ts +47 -0
- package/src/middleware/sitemap-middleware.test.ts +152 -0
- package/src/middleware/sitemap-middleware.ts +65 -0
- package/src/services/component-props-service.ts +182 -0
- package/src/sharedTypes/component-props.ts +17 -0
- package/src/site/index.ts +1 -0
- package/src/test-data/components/Bar.astro +0 -0
- package/src/test-data/components/Baz.astro +0 -0
- package/src/test-data/components/Foo.astro +0 -0
- package/src/test-data/components/Hero.variant.astro +0 -0
- package/src/test-data/components/NotComponent.bsx +0 -0
- package/src/test-data/components/Qux.astro +0 -0
- package/src/test-data/components/folded/Folded.astro +0 -0
- package/src/test-data/components/folded/random-file-2.docx +0 -0
- package/src/test-data/components/random-file.txt +0 -0
- package/src/test-data/helpers.ts +46 -0
- package/src/test-data/personalizeData.ts +63 -0
- package/src/tools/generate-map.ts +83 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/templating/components.test.ts +305 -0
- package/src/tools/templating/components.ts +49 -0
- package/src/tools/templating/constants.ts +4 -0
- package/src/tools/templating/default-component.test.ts +31 -0
- package/src/tools/templating/default-component.ts +63 -0
- package/src/tools/templating/index.ts +2 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/utils.test.ts +48 -0
- package/src/utils/utils.ts +52 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/* eslint-disable dot-notation */
|
|
2
|
+
/* eslint-disable no-unused-expressions, @typescript-eslint/no-unused-expressions */
|
|
3
|
+
import * as chai from 'chai';
|
|
4
|
+
import { use } from 'chai';
|
|
5
|
+
import sinonChai from 'sinon-chai';
|
|
6
|
+
import sinon from 'sinon';
|
|
7
|
+
import chaiString from 'chai-string';
|
|
8
|
+
import { MiddlewareBase, REWRITE_HEADER_NAME } from './middleware';
|
|
9
|
+
import { SiteResolver } from '../site';
|
|
10
|
+
import { PreviewCookies } from '../editing';
|
|
11
|
+
import { APIContext, AstroCookieSetOptions, MiddlewareHandler, RewritePayload } from 'astro';
|
|
12
|
+
|
|
13
|
+
use(sinonChai);
|
|
14
|
+
const expect = chai.use(chaiString).expect;
|
|
15
|
+
|
|
16
|
+
class MockSiteResolver extends SiteResolver {
|
|
17
|
+
getByName = sinon.stub().callsFake((siteName: string) => ({
|
|
18
|
+
name: siteName,
|
|
19
|
+
language: 'en',
|
|
20
|
+
hostName: 'foo.net',
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
getByHost = sinon.stub().callsFake((hostName: string) => ({
|
|
24
|
+
name: 'foo',
|
|
25
|
+
language: 'en',
|
|
26
|
+
hostName,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const createContext = (props: any = {}) => {
|
|
31
|
+
const context = {
|
|
32
|
+
request: {
|
|
33
|
+
url: props?.url,
|
|
34
|
+
headers: {
|
|
35
|
+
get(key: string) {
|
|
36
|
+
const headers = {
|
|
37
|
+
...context.request.headers,
|
|
38
|
+
...props?.headerValues,
|
|
39
|
+
};
|
|
40
|
+
return headers[key];
|
|
41
|
+
},
|
|
42
|
+
append(key: string, value: string | Record<string, any>) {
|
|
43
|
+
context.request.headers[key] = value;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
cookies: {
|
|
48
|
+
get(cookieName: string) {
|
|
49
|
+
const cookies = { ...props?.cookieValues };
|
|
50
|
+
return cookies[cookieName] ? { value: cookies[cookieName] } : undefined;
|
|
51
|
+
},
|
|
52
|
+
set(
|
|
53
|
+
cookieName: string,
|
|
54
|
+
value: string | Record<string, any>,
|
|
55
|
+
options?: AstroCookieSetOptions
|
|
56
|
+
) {
|
|
57
|
+
context.cookies[cookieName] = { value, ...options };
|
|
58
|
+
},
|
|
59
|
+
...props?.cookies,
|
|
60
|
+
...props.cookieValues,
|
|
61
|
+
},
|
|
62
|
+
locals: props.locals || {},
|
|
63
|
+
url: props?.url,
|
|
64
|
+
currentLocale: props.currentLocale,
|
|
65
|
+
preferredLocale: props.preferredLocale,
|
|
66
|
+
// eslint-disable-next-line no-unused-vars
|
|
67
|
+
rewrite: (_) => props.response,
|
|
68
|
+
} as APIContext;
|
|
69
|
+
|
|
70
|
+
return context;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const createResponse = (props: any = {}) => {
|
|
74
|
+
const response = {
|
|
75
|
+
...props,
|
|
76
|
+
url: props.url,
|
|
77
|
+
headers: {
|
|
78
|
+
get(key: string) {
|
|
79
|
+
const headers = {
|
|
80
|
+
...response.headers,
|
|
81
|
+
...props.headerValues,
|
|
82
|
+
};
|
|
83
|
+
return headers[key];
|
|
84
|
+
},
|
|
85
|
+
append(key: string, value: string | Record<string, any>) {
|
|
86
|
+
response.headers[key] = value;
|
|
87
|
+
},
|
|
88
|
+
...props.headers,
|
|
89
|
+
},
|
|
90
|
+
} as Response;
|
|
91
|
+
|
|
92
|
+
return response;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
describe('MiddlewareBase', () => {
|
|
96
|
+
class SampleMiddleware extends MiddlewareBase {
|
|
97
|
+
handle: MiddlewareHandler = () => {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe('defaultHostname', () => {
|
|
101
|
+
it('should set default hostname', () => {
|
|
102
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
103
|
+
|
|
104
|
+
expect(middleware['defaultHostname']).to.equal('localhost');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should set custom hostname', () => {
|
|
108
|
+
const middleware = new SampleMiddleware({
|
|
109
|
+
sites: [],
|
|
110
|
+
defaultHostname: 'foo',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(middleware['defaultHostname']).to.equal('foo');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('isPreview', () => {
|
|
118
|
+
it('should return true when preview data cookie is provided', () => {
|
|
119
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
120
|
+
const context = createContext({
|
|
121
|
+
cookieValues: {
|
|
122
|
+
[PreviewCookies.PREVIEW_DATA]: 'value',
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(middleware['isPreview'](context)).to.equal(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should return false when required cookie is not provided', () => {
|
|
130
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
131
|
+
const context = createContext({});
|
|
132
|
+
|
|
133
|
+
expect(middleware['isPreview'](context)).to.equal(false);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
/*
|
|
137
|
+
describe('isPrefetch', () => {
|
|
138
|
+
it('should return true when purpose header is prefetch', () => {
|
|
139
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
140
|
+
const req = createReq({
|
|
141
|
+
headerValues: {
|
|
142
|
+
purpose: 'prefetch',
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(middleware['isPrefetch'](req)).to.equal(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should return true when Next-Router-Prefetch header is 1', () => {
|
|
150
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
151
|
+
const req = createReq({
|
|
152
|
+
headerValues: {
|
|
153
|
+
'Next-Router-Prefetch': '1',
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(middleware['isPrefetch'](req)).to.equal(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should return true when x-middleware-prefetch header is 1', () => {
|
|
161
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
162
|
+
const req = createReq({
|
|
163
|
+
headerValues: {
|
|
164
|
+
'x-middleware-prefetch': '1',
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(middleware['isPrefetch'](req)).to.equal(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should return false when required header is not provided', () => {
|
|
172
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
173
|
+
const req = createReq();
|
|
174
|
+
|
|
175
|
+
expect(middleware['isPrefetch'](req)).to.equal(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('returns false for known device with x-middleware-prefetch header', () => {
|
|
179
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
180
|
+
const req = createReq({
|
|
181
|
+
headerValues: {
|
|
182
|
+
'x-middleware-prefetch': '1',
|
|
183
|
+
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X)',
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(middleware['isPrefetch'](req)).to.equal(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should return true when it is a desktop device and purpose is prefetch', () => {
|
|
191
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
192
|
+
const req = createReq({
|
|
193
|
+
headerValues: {
|
|
194
|
+
purpose: 'prefetch',
|
|
195
|
+
'sec-ch-ua-mobile': '?0',
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(middleware['isPrefetch'](req)).to.equal(true);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
*/
|
|
203
|
+
describe('disabled / skip', () => {
|
|
204
|
+
it('default', () => {
|
|
205
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
206
|
+
|
|
207
|
+
expect(
|
|
208
|
+
middleware['disabled'](
|
|
209
|
+
createContext({
|
|
210
|
+
url: {
|
|
211
|
+
pathname: '/api/layout/render',
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
createResponse()
|
|
215
|
+
)
|
|
216
|
+
).to.equal(true);
|
|
217
|
+
|
|
218
|
+
expect(
|
|
219
|
+
middleware['disabled'](
|
|
220
|
+
createContext({
|
|
221
|
+
url: {
|
|
222
|
+
pathname: '/sitecore/render',
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
createResponse()
|
|
226
|
+
)
|
|
227
|
+
).to.equal(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('custom function', () => {
|
|
231
|
+
const middleware = new SampleMiddleware({
|
|
232
|
+
sites: [],
|
|
233
|
+
skip(context: APIContext) {
|
|
234
|
+
return context.url.pathname === 'foo';
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(
|
|
239
|
+
middleware['disabled'](
|
|
240
|
+
createContext({
|
|
241
|
+
url: {
|
|
242
|
+
pathname: 'bar',
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
createResponse()
|
|
246
|
+
)
|
|
247
|
+
).to.equal(false);
|
|
248
|
+
expect(
|
|
249
|
+
middleware['disabled'](
|
|
250
|
+
createContext({
|
|
251
|
+
url: {
|
|
252
|
+
pathname: 'foo',
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
createResponse()
|
|
256
|
+
)
|
|
257
|
+
).to.equal(true);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('disabled in chain', () => {
|
|
262
|
+
it('should return false if skipMiddleware local variable is not set or false', () => {
|
|
263
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
264
|
+
|
|
265
|
+
expect(
|
|
266
|
+
middleware['disabledInChain'](
|
|
267
|
+
createContext({
|
|
268
|
+
url: {
|
|
269
|
+
pathname: '/api/layout/render',
|
|
270
|
+
},
|
|
271
|
+
locals: {
|
|
272
|
+
skipMiddleware: false,
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
createResponse()
|
|
276
|
+
)
|
|
277
|
+
).to.equal(false);
|
|
278
|
+
|
|
279
|
+
expect(
|
|
280
|
+
middleware['disabledInChain'](
|
|
281
|
+
createContext({
|
|
282
|
+
url: {
|
|
283
|
+
pathname: '/api/layout/render',
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
createResponse()
|
|
287
|
+
)
|
|
288
|
+
).to.equal(false);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should return true if skipMiddleware local variable is true', () => {
|
|
292
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
293
|
+
|
|
294
|
+
expect(
|
|
295
|
+
middleware['disabledInChain'](
|
|
296
|
+
createContext({
|
|
297
|
+
url: {
|
|
298
|
+
pathname: '/api/layout/render',
|
|
299
|
+
},
|
|
300
|
+
locals: {
|
|
301
|
+
skipMiddleware: true,
|
|
302
|
+
},
|
|
303
|
+
}),
|
|
304
|
+
createResponse()
|
|
305
|
+
)
|
|
306
|
+
).to.equal(true);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('extractDebugHeaders', () => {
|
|
311
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
312
|
+
|
|
313
|
+
const headers = new Headers({});
|
|
314
|
+
headers.set('foo', 'net');
|
|
315
|
+
headers.set('bar', 'one');
|
|
316
|
+
|
|
317
|
+
expect(middleware['extractDebugHeaders'](headers)).to.deep.equal({
|
|
318
|
+
foo: 'net',
|
|
319
|
+
bar: 'one',
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('getHostHeader', () => {
|
|
324
|
+
it('should return default hostname when header is not present', () => {
|
|
325
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
326
|
+
const context = createContext({
|
|
327
|
+
headerValues: {
|
|
328
|
+
foo: 'one',
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(middleware['getHostHeader'](context)).to.equal(undefined);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should return host header', () => {
|
|
336
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
337
|
+
const context = createContext({
|
|
338
|
+
headerValues: {
|
|
339
|
+
foo: 'one',
|
|
340
|
+
host: 'bar.net:9999',
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
expect(middleware['getHostHeader'](context)).to.equal('bar.net');
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('getLanguage', () => {
|
|
349
|
+
it('should return defined language', () => {
|
|
350
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
351
|
+
const context = createContext({
|
|
352
|
+
currentLocale: 'be',
|
|
353
|
+
preferredLocale: 'fr',
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(middleware['getLanguage'](context)).to.equal('be');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should return defined default language', () => {
|
|
360
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
361
|
+
const context = createContext({
|
|
362
|
+
preferredLocale: 'fr',
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
expect(middleware['getLanguage'](context)).to.equal('fr');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should use fallback language from config when present', () => {
|
|
369
|
+
const middleware = new SampleMiddleware({
|
|
370
|
+
sites: [],
|
|
371
|
+
defaultLanguage: 'es-ES',
|
|
372
|
+
});
|
|
373
|
+
const context = createContext();
|
|
374
|
+
|
|
375
|
+
expect(middleware['getLanguage'](context)).to.equal('es-ES');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should return fallback language', () => {
|
|
379
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
380
|
+
const context = createContext();
|
|
381
|
+
|
|
382
|
+
expect(middleware['getLanguage'](context)).to.equal('en');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe('getSite', () => {
|
|
387
|
+
it('should get site by name when site cookie is provided', () => {
|
|
388
|
+
const context = createContext();
|
|
389
|
+
|
|
390
|
+
const res = createResponse({
|
|
391
|
+
headers: {
|
|
392
|
+
'Set-Cookie': 'sc_site=xxx',
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
397
|
+
middleware['siteResolver'] = new MockSiteResolver([]);
|
|
398
|
+
|
|
399
|
+
expect(middleware['getSite'](context, res).name).to.equal('xxx');
|
|
400
|
+
expect(middleware['siteResolver'].getByName).to.be.calledWith('xxx');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('should get default site info when site cookie is provided', () => {
|
|
404
|
+
class MockSiteResolver extends SiteResolver {
|
|
405
|
+
// eslint-disable-next-line no-unused-vars
|
|
406
|
+
getByName = sinon.stub().callsFake((_siteName: string) => undefined);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const context = createContext();
|
|
410
|
+
|
|
411
|
+
const res = createResponse({
|
|
412
|
+
headers: {
|
|
413
|
+
'Set-Cookie': 'sc_site=xxx',
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
418
|
+
middleware['siteResolver'] = new MockSiteResolver([]);
|
|
419
|
+
|
|
420
|
+
expect(middleware['getSite'](context, res)).deep.equal({
|
|
421
|
+
name: 'xxx',
|
|
422
|
+
language: 'en',
|
|
423
|
+
hostName: '*',
|
|
424
|
+
});
|
|
425
|
+
expect(middleware['siteResolver'].getByName).to.be.calledWith('xxx');
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should get site by host header', () => {
|
|
430
|
+
const context = createContext({
|
|
431
|
+
headerValues: {
|
|
432
|
+
host: 'xxx.net:9999',
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
436
|
+
middleware['siteResolver'] = new MockSiteResolver([]);
|
|
437
|
+
|
|
438
|
+
expect(middleware['getSite'](context).hostName).to.equal('xxx.net');
|
|
439
|
+
expect(middleware['siteResolver'].getByHost).to.be.calledWith('xxx.net');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('should get site by default host', () => {
|
|
443
|
+
const context = createContext();
|
|
444
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
445
|
+
middleware['siteResolver'] = new MockSiteResolver([]);
|
|
446
|
+
|
|
447
|
+
expect(middleware['getSite'](context).hostName).to.equal('localhost');
|
|
448
|
+
expect(middleware['siteResolver'].getByHost).to.be.calledWith('localhost');
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('should get site by custom default host', () => {
|
|
452
|
+
const context = createContext();
|
|
453
|
+
|
|
454
|
+
const middleware = new SampleMiddleware({
|
|
455
|
+
sites: [],
|
|
456
|
+
defaultHostname: 'yyy.net',
|
|
457
|
+
});
|
|
458
|
+
middleware['siteResolver'] = new MockSiteResolver([]);
|
|
459
|
+
|
|
460
|
+
expect(middleware['getSite'](context).hostName).to.equal('yyy.net');
|
|
461
|
+
expect(middleware['siteResolver'].getByHost).to.be.calledWith('yyy.net');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
describe('rewrite', () => {
|
|
465
|
+
it('should add header by default', async () => {
|
|
466
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
467
|
+
const url = new URL('http://localhost:3000/not-found');
|
|
468
|
+
|
|
469
|
+
const context = createContext({
|
|
470
|
+
url: url,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const mockNext = async (rewritePath?: RewritePayload) => {
|
|
474
|
+
return createResponse({
|
|
475
|
+
url: rewritePath,
|
|
476
|
+
headers: [],
|
|
477
|
+
});
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const response = await middleware['rewrite']('/new', context, mockNext);
|
|
481
|
+
|
|
482
|
+
expect(response.headers.get(REWRITE_HEADER_NAME)).to.equal('/new');
|
|
483
|
+
expect(response.url.toString()).to.endWith('/new');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should not rewrite header when skipHeader is true', async () => {
|
|
487
|
+
const middleware = new SampleMiddleware({ sites: [] });
|
|
488
|
+
const url = new URL('http://localhost:3000/not-found');
|
|
489
|
+
|
|
490
|
+
const context = createContext({
|
|
491
|
+
url: url,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const mockNext = async (rewritePath?: RewritePayload) => {
|
|
495
|
+
return createResponse({
|
|
496
|
+
url: rewritePath,
|
|
497
|
+
headers: [],
|
|
498
|
+
});
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const response = await middleware['rewrite']('/new', context, mockNext, true);
|
|
502
|
+
|
|
503
|
+
expect(response.headers.get(REWRITE_HEADER_NAME)).to.be.undefined;
|
|
504
|
+
expect(response.url.toString()).to.endWith('/new');
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { SITE_KEY, SiteInfo, SiteResolver } from '@sitecore-content-sdk/core/site';
|
|
2
|
+
import { GraphQLRequestClientFactory } from '@sitecore-content-sdk/core';
|
|
3
|
+
import {
|
|
4
|
+
createGraphQLClientFactory,
|
|
5
|
+
GraphQLClientOptions,
|
|
6
|
+
} from '@sitecore-content-sdk/core/client';
|
|
7
|
+
import { PreviewCookies } from '../editing';
|
|
8
|
+
import { APIContext, MiddlewareHandler, MiddlewareNext } from 'astro';
|
|
9
|
+
import * as cookie from 'cookie';
|
|
10
|
+
|
|
11
|
+
export const REWRITE_HEADER_NAME = 'x-sc-rewrite';
|
|
12
|
+
|
|
13
|
+
export type MiddlewareBaseConfig = {
|
|
14
|
+
/**
|
|
15
|
+
* function, determines if middleware execution should be skipped, based on cookie, header, or other considerations
|
|
16
|
+
* @param {APIContext} context the Astro context
|
|
17
|
+
*/
|
|
18
|
+
skip?: (context: APIContext) => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Fallback hostname in case `host` header is not present
|
|
21
|
+
* @default localhost
|
|
22
|
+
*/
|
|
23
|
+
defaultHostname?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Fallback language in locale cannot be extracted from request URL
|
|
26
|
+
* @default 'en'
|
|
27
|
+
*/
|
|
28
|
+
defaultLanguage?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Site resolution implementation by name/hostname
|
|
31
|
+
*/
|
|
32
|
+
sites: SiteInfo[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Middleware class to be extended by all middleware implementations
|
|
37
|
+
*/
|
|
38
|
+
export abstract class Middleware {
|
|
39
|
+
/**
|
|
40
|
+
* Handler method to execute middleware logic
|
|
41
|
+
* @param {MiddlewareHandler} handler MiddlewareHandler
|
|
42
|
+
*/
|
|
43
|
+
abstract handle: MiddlewareHandler;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Base middleware class with common methods
|
|
48
|
+
*/
|
|
49
|
+
export abstract class MiddlewareBase extends Middleware {
|
|
50
|
+
protected defaultHostname: string;
|
|
51
|
+
protected siteResolver: SiteResolver;
|
|
52
|
+
|
|
53
|
+
constructor(protected config: MiddlewareBaseConfig) {
|
|
54
|
+
super();
|
|
55
|
+
this.siteResolver = new SiteResolver(config.sites);
|
|
56
|
+
this.defaultHostname = config.defaultHostname || 'localhost';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Determines if mode is preview
|
|
61
|
+
* @param {APIContext} context Astro context
|
|
62
|
+
* @returns {boolean} is preview
|
|
63
|
+
*/
|
|
64
|
+
protected isPreview(context: APIContext) {
|
|
65
|
+
return !!context.cookies.get(PreviewCookies.PREVIEW_DATA);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
protected disabled(context: APIContext) {
|
|
69
|
+
const { pathname } = context.url;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
pathname.startsWith('/api/') || // Ignore API calls
|
|
73
|
+
pathname.startsWith('/sitecore/') || // Ignore Sitecore API calls
|
|
74
|
+
(this.config.skip && this.config.skip(context))
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected disabledInChain(context: APIContext) {
|
|
79
|
+
return context.locals.skipMiddleware || false; // Skip if disabled in one of the previous middlewares in chain
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Safely extract all headers for debug logging
|
|
84
|
+
* Necessary to avoid middleware issue https://github.com/vercel/next.js/issues/39765
|
|
85
|
+
* @param {Headers} incomingHeaders Incoming headers
|
|
86
|
+
* @returns Object with headers as key/value pairs
|
|
87
|
+
*/
|
|
88
|
+
protected extractDebugHeaders(incomingHeaders: Headers) {
|
|
89
|
+
const headers = {} as { [key: string]: string };
|
|
90
|
+
incomingHeaders.forEach((value, key) => (headers[key] = value));
|
|
91
|
+
return headers;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Provides used language
|
|
96
|
+
* @param {APIContext} context Astro context
|
|
97
|
+
* @returns {string} language
|
|
98
|
+
*/
|
|
99
|
+
protected getLanguage(context: APIContext) {
|
|
100
|
+
return context.currentLocale || context.preferredLocale || this.config.defaultLanguage || 'en';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Extract 'host' header
|
|
105
|
+
* @param {APIContext} context Astro context
|
|
106
|
+
*/
|
|
107
|
+
protected getHostHeader(context: APIContext) {
|
|
108
|
+
return context.request.headers.get('host')?.split(':')[0];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get site information. If site name is stored in cookie, use it, otherwise resolve by hostname
|
|
113
|
+
* - If site can't be resolved by site name cookie use default site info based on provided parameters
|
|
114
|
+
* - If site can't be resolved by hostname throw an error
|
|
115
|
+
* @param {APIContext} context Astro context
|
|
116
|
+
* @param {Response} [res] response
|
|
117
|
+
* @returns {SiteInfo} site information
|
|
118
|
+
*/
|
|
119
|
+
protected getSite(context: APIContext, res?: Response): SiteInfo {
|
|
120
|
+
const siteNameCookie = cookie.parse(res?.headers.get('Set-Cookie') || '')[SITE_KEY];
|
|
121
|
+
const hostname = this.getHostHeader(context) || this.defaultHostname;
|
|
122
|
+
|
|
123
|
+
if (siteNameCookie) {
|
|
124
|
+
// Usually we should be able to resolve site by cookie
|
|
125
|
+
// in case of Sitecore Preview mode, there can be a case that new site was created
|
|
126
|
+
// but it's not present in the sitemap, so we fallback to default site info
|
|
127
|
+
return (
|
|
128
|
+
this.siteResolver.getByName(siteNameCookie) || {
|
|
129
|
+
name: siteNameCookie,
|
|
130
|
+
language: this.getLanguage(context),
|
|
131
|
+
hostName: '*',
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return this.siteResolver.getByHost(hostname);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
protected getClientFactory(graphQLOptions: GraphQLClientOptions): GraphQLRequestClientFactory {
|
|
140
|
+
return createGraphQLClientFactory(graphQLOptions);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create a rewrite response
|
|
145
|
+
* @param {string} rewritePath the destionation path
|
|
146
|
+
* @param {APIContext} context the middleware context
|
|
147
|
+
* @param {MiddlewareNext} next the middleware object to execute rewrite
|
|
148
|
+
* @param {boolean} [skipHeader] don't write 'x-sc-rewrite' header
|
|
149
|
+
*/
|
|
150
|
+
protected async rewrite(
|
|
151
|
+
rewritePath: string,
|
|
152
|
+
context: APIContext,
|
|
153
|
+
next: MiddlewareNext,
|
|
154
|
+
skipHeader?: boolean
|
|
155
|
+
): Promise<Response> {
|
|
156
|
+
const url = new URL(context.url);
|
|
157
|
+
url.pathname = rewritePath;
|
|
158
|
+
const response = await next(url);
|
|
159
|
+
|
|
160
|
+
// Share rewrite path with following executed middlewares
|
|
161
|
+
if (!skipHeader) {
|
|
162
|
+
response.headers.append(REWRITE_HEADER_NAME, rewritePath);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return response;
|
|
166
|
+
}
|
|
167
|
+
}
|