@exdst-sitecore-content-sdk/astro 0.0.22 → 0.0.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exdst-sitecore-content-sdk/astro",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "publishConfig": {
@@ -15,7 +15,7 @@
15
15
  "test:components": "vitest run --config ./vitest.config.ts",
16
16
  "coverage": "nyc npm test",
17
17
  "generate-docs": "npx typedoc --plugin typedoc-plugin-markdown --outputFileStrategy Members --parametersFormat table --readme none --out ../../ref-docs/core --entryPoints src/index.ts --entryPoints src/config/index.ts --entryPoints src/client/index.ts --entryPoints src/i18n/index.ts --entryPoints src/layout/index.ts --entryPoints src/media/index.ts --entryPoints src/personalize/index.ts --entryPoints src/site/index.ts --entryPoints src/tracking/index.ts --entryPoints src/utils/index.ts --entryPoints src/editing/index.ts --entryPoints src/tools/index.ts --githubPages false",
18
- "api-extractor": "npm run build && api-extractor run --local --verbose",
18
+ "api-extractor": "api-extractor run --local --verbose",
19
19
  "api-extractor:verify": "api-extractor run"
20
20
  },
21
21
  "engines": {
@@ -49,7 +49,7 @@
49
49
  "@types/sinon-chai": "^4.0.0",
50
50
  "@typescript-eslint/eslint-plugin": "8.39.0",
51
51
  "@typescript-eslint/parser": "8.39.0",
52
- "astro": "^6.1.1",
52
+ "astro": "^6.1.3",
53
53
  "chai": "^4.4.1",
54
54
  "chai-spies": "^1.1.0",
55
55
  "chai-string": "^1.6.0",
@@ -34,9 +34,14 @@ describe('SitecoreClient', () => {
34
34
  },
35
35
  defaultSite: 'default-site',
36
36
  defaultLanguage: 'en',
37
+ multisite: {
38
+ enabled: true,
39
+ useCookieResolution: () => true,
40
+ },
37
41
  layout: { formatLayoutQuery: sandbox.stub() },
38
42
  dictionary: { caching: { enabled: true, timeout: 60000 } },
39
43
  disableCodeGeneration: false,
44
+ rewriteMediaUrls: false,
40
45
  };
41
46
 
42
47
  let sitecoreClient = new SitecoreAstroClient(defaultInitOptions);
@@ -97,12 +102,6 @@ describe('SitecoreClient', () => {
97
102
  const path = `${VARIANT_PREFIX}variant1/${VARIANT_PREFIX}mountain_bike_audience/test/path`;
98
103
  const locale = 'en-US';
99
104
  const testLayoutData = structuredClone(layoutData);
100
-
101
- const siteInfo = {
102
- name: 'default-site',
103
- hostName: 'example.com',
104
- language: 'en',
105
- };
106
105
  layoutServiceStub.fetchLayoutData.returns(testLayoutData);
107
106
  sandbox.stub(sitecoreClient, 'getHeadLinks').returns([]);
108
107
 
@@ -117,12 +116,6 @@ describe('SitecoreClient', () => {
117
116
  const path = `${VARIANT_PREFIX}variant1/${VARIANT_PREFIX}sand_bike_audience/test/path`;
118
117
  const locale = 'en-US';
119
118
  const testLayoutData = structuredClone(layoutData);
120
-
121
- const siteInfo = {
122
- name: 'default-site',
123
- hostName: 'example.com',
124
- language: 'en',
125
- };
126
119
  layoutServiceStub.fetchLayoutData.returns(testLayoutData);
127
120
  sandbox.stub(sitecoreClient, 'getHeadLinks').returns([]);
128
121
 
@@ -248,45 +241,27 @@ describe('SitecoreClient', () => {
248
241
  });
249
242
  });
250
243
 
251
- /*
252
- describe('getComponentData', () => {
253
- it('should return componentData when component has getComponentsProps method', async () => {
254
- const context = {
255
- params: { path: ['test', 'path'] },
256
- query: {},
257
- req: {},
258
- res: {},
259
- resolvedUrl: '/test/path',
260
- } as unknown as GetServerSidePropsContext;
261
- const layoutData = {
262
- sitecore: {
263
- context,
264
- route: {
265
- name: 'test',
266
- placeholders: {
267
- main: [
268
- {
269
- componentName: 'TestComponent',
270
- uid: 'test-uid',
271
- },
272
- ],
273
- },
274
- },
275
- },
276
- };
277
-
278
- const mockComponent = {
279
- getComponentServerProps: sandbox.stub().resolves({ props: { data: 'test-data' } }),
280
- };
244
+ describe('getPreview', () => {
245
+ it('should call base getPreview with preview data and fetch options', async () => {
246
+ const previewData = {
247
+ variantId: 'variant-a',
248
+ route: '/test/path',
249
+ language: 'en',
250
+ } as any;
251
+ const fetchOptions = {
252
+ retries: 2,
253
+ } as any;
281
254
 
282
- const componentMap = new Map([['TestComponent', mockComponent]]);
255
+ const basePrototype = Object.getPrototypeOf(SitecoreAstroClient.prototype);
256
+ const superGetPreviewStub = sandbox.stub(basePrototype, 'getPreview').resolves(null);
283
257
 
284
- const result = await sitecoreClient.getComponentData(layoutData, context, componentMap);
258
+ try {
259
+ await sitecoreClient.getPreview(previewData, fetchOptions);
285
260
 
286
- expect(result).to.deep.equal({
287
- 'test-uid': { props: { data: 'test-data' } },
288
- });
289
- expect(mockComponent.getComponentServerProps.calledOnce).to.be.true;
261
+ expect(superGetPreviewStub).to.have.been.calledOnceWithExactly(previewData, fetchOptions);
262
+ } finally {
263
+ superGetPreviewStub.restore();
264
+ }
290
265
  });
291
- });*/
266
+ });
292
267
  });
@@ -7,7 +7,6 @@ import {
7
7
  SitecoreClientInit,
8
8
  } from '@sitecore-content-sdk/content/client';
9
9
  import { PreviewData } from '../sharedTypes/component-props';
10
- import { ComponentPropsService } from '../services/component-props-service';
11
10
  import { EditingPreviewData } from '@sitecore-content-sdk/content/editing';
12
11
  import { getSiteRewriteData, normalizeSiteRewrite } from '@sitecore-content-sdk/content/site';
13
12
  import {
@@ -27,10 +26,8 @@ export type SitecoreAstroClientInit = SitecoreClientInit & Pick<SitecoreConfig,
27
26
  * @public
28
27
  */
29
28
  export class SitecoreAstroClient extends SitecoreClient {
30
- protected componentPropsService: ComponentPropsService;
31
29
  constructor(protected initOptions: SitecoreAstroClientInit) {
32
30
  super(initOptions);
33
- this.componentPropsService = this.getComponentPropsService();
34
31
  }
35
32
 
36
33
  /**
@@ -109,46 +106,4 @@ export class SitecoreAstroClient extends SitecoreClient {
109
106
 
110
107
  return staticPaths;
111
108
  }
112
-
113
- // /**
114
- // * Parses components from component map and layoutData, executes getServerProps/getStaticProps methods
115
- // * and returns resulting props from components
116
- // * @param {LayoutServiceData} layoutData layout data to parse compnents from
117
- // * @param {ComponentMap<AstroContentSdkComponent>} components component map to get props for
118
- // * @returns {ComponentPropsCollection} component props
119
- // */
120
- // async getComponentData(
121
- // layoutData: LayoutServiceData,
122
- // // context: GetServerSidePropsContext | GetStaticPropsContext,
123
- // components: ComponentMap<AstroContentSdkComponent>
124
- // ): Promise<ComponentPropsCollection> {
125
- // let componentProps: ComponentPropsCollection = {};
126
- // if (!layoutData.sitecore.route) return componentProps;
127
- // // Retrieve component props using side-effects defined on components level
128
- // componentProps = await this.componentPropsService.fetchComponentProps({
129
- // layoutData: layoutData,
130
- // // context,
131
- // components,
132
- // });
133
-
134
- // const errors = Object.keys(componentProps)
135
- // .map((id) => {
136
- // const component = componentProps[id] as ComponentPropsError;
137
-
138
- // return component.error
139
- // ? `\nUnable to get component props for ${component.componentName} (${id}): ${component.error}`
140
- // : '';
141
- // })
142
- // .join('');
143
-
144
- // if (errors.length) {
145
- // throw new Error(errors);
146
- // }
147
-
148
- // return componentProps;
149
- // }
150
-
151
- protected getComponentPropsService(): ComponentPropsService {
152
- return new ComponentPropsService();
153
- }
154
109
  }
@@ -11,10 +11,7 @@ describe('defineCliConfig', () => {
11
11
  const validateDefaultTemplates = (result: SitecoreCliConfig) => {
12
12
  expect(result.scaffold.templates[0].name).to.equal(ComponentTemplateType.DEFAULT);
13
13
  const defaultTemplate = result.scaffold.templates[0].generateTemplate('ComponentName');
14
- // expect(defaultTemplate).to.contain(
15
- // // eslint-disable-next-line quotes
16
- // `import { ComponentParams, ComponentRendering } from '@sitecore-content-sdk/nextjs';`
17
- // );
14
+
18
15
  expect(defaultTemplate).to.contain('ComponentName');
19
16
  if (result.scaffold.templates[0].getNextSteps) {
20
17
  const componentpath = 'src/components/ComponentName.astro';
@@ -22,24 +19,6 @@ describe('defineCliConfig', () => {
22
19
  `* Implement the Astro component in ${chalk.green(componentpath)}`
23
20
  );
24
21
  }
25
-
26
- // expect(result.scaffold.templates[1].name).to.equal(ComponentTemplateType.BYOC);
27
- // const byocTemplate = result.scaffold.templates[1].generateTemplate('ByocComponentName');
28
- // expect(byocTemplate).to.contain(
29
- // // eslint-disable-next-line quotes
30
- // `import * as FEAAS from '@sitecore-feaas/clientside/react';`
31
- // );
32
- // expect(byocTemplate).to.contain('ByocComponentName');
33
-
34
- // expect(result.scaffold.templates[1].generateTemplate('comp name')).to.contain(
35
- // // eslint-disable-next-line quotes
36
- // `import * as FEAAS from '@sitecore-feaas/clientside/react';`
37
- // );
38
- // if (result.scaffold.templates[1].getNextSteps) {
39
- // expect(result.scaffold.templates[1].getNextSteps('componentpath')[0]).to.contain(
40
- // '* Modify component registration through FEAAS.External.registerComponent if needed'
41
- // );
42
- // }
43
22
  };
44
23
 
45
24
  it('should add default and byoc scaffold templates', () => {
package/src/context.ts CHANGED
@@ -12,7 +12,7 @@ export const SitecoreContext: any = map({});
12
12
 
13
13
  /**
14
14
  * Shape of values passed when updating {@link SitecoreContext} (page, API, and optional component map).
15
- * @internal
15
+ * @public
16
16
  */
17
17
  export interface SitecoreContextProps {
18
18
  /**
@@ -31,9 +31,9 @@ export interface SitecoreContextProps {
31
31
 
32
32
  /**
33
33
  * Shape of values passed when updating dictionary phrases on {@link SitecoreContext}.
34
- * @internal
34
+ * @public
35
35
  */
36
- export interface SitecoreDictionarytProps {
36
+ export interface SitecoreDictionaryProps {
37
37
  /**
38
38
  * The dictionary data.
39
39
  */
@@ -42,10 +42,8 @@ export interface SitecoreDictionarytProps {
42
42
 
43
43
  /**
44
44
  * Writes page data, API config, and optional component map into {@link SitecoreContext}.
45
- * @param {Page} props.page - The page data.
46
- * @param {SitecoreConfig['api']} props.api - The API configuration.
47
- * @param {ComponentMap} [props.componentMap] - Component map.
48
- * @internal
45
+ * @param {SitecoreContextProps} props - Page, API, and optional component map.
46
+ * @public
49
47
  */
50
48
  export const updateSitecoreContext = ({ page, api, componentMap }: SitecoreContextProps) => {
51
49
  SitecoreContext.setKey('page', page);
@@ -55,11 +53,10 @@ export const updateSitecoreContext = ({ page, api, componentMap }: SitecoreConte
55
53
 
56
54
  /**
57
55
  * Writes dictionary phrases into {@link SitecoreContext} for {@link useDictionary}.
58
- * @param {SitecoreDictionarytProps} props
59
- * @param {DictionaryPhrases} props.dictionary
60
- * @internal
56
+ * @param {DictionaryPhrases} dictionary - The dictionary data.
57
+ * @public
61
58
  */
62
- export const updateSitecoreDictionary = ({ dictionary }: SitecoreDictionarytProps) => {
59
+ export const updateSitecoreDictionary = ({ dictionary }: SitecoreDictionaryProps) => {
63
60
  SitecoreContext.setKey('dictionary', dictionary);
64
61
  };
65
62
 
@@ -68,9 +65,10 @@ export const updateSitecoreDictionary = ({ dictionary }: SitecoreDictionarytProp
68
65
  * @public
69
66
  */
70
67
  export const useSitecore = (): SitecoreContextProps => {
68
+ const context = SitecoreContext.get();
71
69
  return {
72
- page: SitecoreContext.get()['page'],
73
- api: SitecoreContext.get()['api'],
70
+ page: context.page,
71
+ api: context.api,
74
72
  };
75
73
  };
76
74
 
@@ -79,7 +77,7 @@ export const useSitecore = (): SitecoreContextProps => {
79
77
  * @public
80
78
  */
81
79
  export const useComponentMap = (): ComponentMap => {
82
- return SitecoreContext.get()['componentMap'];
80
+ return SitecoreContext.get().componentMap;
83
81
  };
84
82
 
85
83
  /**
@@ -88,7 +86,7 @@ export const useComponentMap = (): ComponentMap => {
88
86
  */
89
87
  export const useDictionary = () => {
90
88
  const t = (key: string): string => {
91
- const dictionary = SitecoreContext.get()['dictionary'];
89
+ const dictionary = SitecoreContext.get().dictionary;
92
90
  if (!dictionary) {
93
91
  return key;
94
92
  }
package/src/env.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable spaced-comment */
2
- /* eslint-disable @typescript-eslint/triple-slash-reference */
2
+ /* eslint-disable no-unused-vars */
3
3
  /// <reference types="astro/client" />
4
4
  declare global {
5
5
  namespace App {
@@ -202,6 +202,33 @@ describe('MultisiteMiddleware', () => {
202
202
  });
203
203
  });
204
204
 
205
+ describe('multisite disabled globally', () => {
206
+ it('should skip when enabled is false and request is not Sitecore preview', async () => {
207
+ const { middleware, siteResolver } = createMiddleware({
208
+ config: { ...defaultConfig, enabled: false },
209
+ });
210
+ const res = createResponse();
211
+
212
+ const context = createContext({
213
+ url: new URL(hostname),
214
+ });
215
+
216
+ const mockNext = async () => res;
217
+
218
+ const finalRes = await middleware.handle(context, mockNext);
219
+
220
+ validateDebugLog('multisite middleware start: %o', {
221
+ pathname: '/styleguide',
222
+ language: 'en',
223
+ hostname: 'foo.net',
224
+ });
225
+ validateDebugLog('skipped (multisite middleware is disabled globally)');
226
+
227
+ expect(finalRes).to.deep.equal(res);
228
+ expect(siteResolver.getByHost.called).to.be.false;
229
+ });
230
+ });
231
+
205
232
  describe('disabled in chain', () => {
206
233
  it('should skip if skipMiddleware local variable is true', async () => {
207
234
  const { middleware } = createMiddleware();
@@ -226,30 +253,33 @@ describe('MultisiteMiddleware', () => {
226
253
  });
227
254
  });
228
255
 
229
- // describe('preview', () => {
230
- // it('preview data cookie is present', async () => {
231
- // const { middleware } = createMiddleware();
232
- // const res = createResponse();
256
+ describe('Vercel/Next preview (_preview_data cookie)', () => {
257
+ it('should skip multisite when preview data cookie is present', async () => {
258
+ const { middleware, siteResolver } = createMiddleware();
259
+ const res = createResponse();
233
260
 
234
- // const context = createContext({
235
- // url: new URL(hostname),
236
- // cookieValues: {
237
- // _preview_data: true,
238
- // },
239
- // });
261
+ const context = createContext({
262
+ url: new URL(hostname),
263
+ cookieValues: {
264
+ _preview_data: '1',
265
+ },
266
+ });
240
267
 
241
- // const mockNext = async () => {
242
- // return res;
243
- // };
268
+ const mockNext = async () => res;
244
269
 
245
- // const finalRes = await middleware.handle(context, mockNext);
270
+ const finalRes = await middleware.handle(context, mockNext);
246
271
 
247
- // validateDebugLog('skipped (preview)');
272
+ validateDebugLog('multisite middleware start: %o', {
273
+ pathname: '/styleguide',
274
+ language: 'en',
275
+ hostname: 'foo.net',
276
+ });
277
+ validateDebugLog('skipped (preview)');
248
278
 
249
- // const resCookies = (finalRes as Response).headers.getSetCookie();
250
- // expect(resCookies).to.contain('_preview_data=true');
251
- // });
252
- // });
279
+ expect(finalRes).to.deep.equal(res);
280
+ expect(siteResolver.getByHost.called).to.be.false;
281
+ });
282
+ });
253
283
 
254
284
  describe('Sitecore Preview', () => {
255
285
  it('request is passed', async () => {
@@ -440,6 +470,45 @@ describe('MultisiteMiddleware', () => {
440
470
  expect(mockNext).calledWith(sinon.match({ pathname: '/_site_foo/styleguide' }));
441
471
  });
442
472
 
473
+ it('x-forwarded-host takes precedence over host for site resolution', async () => {
474
+ const context = createContext({
475
+ headerValues: {
476
+ 'x-forwarded-host': 'forwarded.example.com',
477
+ host: 'foo.net',
478
+ },
479
+ });
480
+
481
+ const mockNext = sinon.stub().returns(
482
+ createResponse({
483
+ headers: [],
484
+ })
485
+ );
486
+
487
+ const { middleware, siteResolver } = createMiddleware();
488
+
489
+ const finalRes = await middleware.handle(context, mockNext);
490
+
491
+ validateDebugLog('multisite middleware start: %o', {
492
+ pathname: '/styleguide',
493
+ language: 'en',
494
+ hostname: 'forwarded.example.com',
495
+ });
496
+
497
+ validateEndMessageDebugLog('multisite middleware end in %dms: %o', {
498
+ rewritePath: '/_site_foo/styleguide',
499
+ siteName: 'foo',
500
+ headers: {
501
+ ...(finalRes as Response).headers,
502
+ 'x-sc-rewrite': '/_site_foo/styleguide',
503
+ },
504
+ cookies: 'sc_site=foo; HttpOnly; Secure; SameSite=None',
505
+ });
506
+
507
+ expect(siteResolver.getByHost).to.be.calledWith('forwarded.example.com');
508
+
509
+ expect(mockNext).calledWith(sinon.match({ pathname: '/_site_foo/styleguide' }));
510
+ });
511
+
443
512
  it('custom response object is not provided', async () => {
444
513
  const context = createContext();
445
514
 
@@ -1,13 +1,3 @@
1
- export type ComponentPropsError = { error: string; componentName: string };
2
-
3
- /**
4
- * Shape of component props storage
5
- * @public
6
- */
7
- export type ComponentPropsCollection = {
8
- [componentUid: string]: unknown | ComponentPropsError;
9
- };
10
-
11
1
  /**
12
2
  * Represents an Astro component import
13
3
  * @public
@@ -0,0 +1,4 @@
1
+ ---
2
+ ---
3
+
4
+ <button type="button">Test Button</button>
@@ -0,0 +1,4 @@
1
+ ---
2
+ ---
3
+
4
+ <a href="#">Test Link</a>
@@ -0,0 +1,201 @@
1
+ /* eslint-disable quotes */
2
+ /* eslint-disable no-unused-expressions, @typescript-eslint/no-unused-expressions */
3
+
4
+ import path from 'path';
5
+ import * as chai from 'chai';
6
+ import sinonChai from 'sinon-chai';
7
+ import { expect } from 'chai';
8
+ import sinon from 'sinon';
9
+
10
+ chai.use(sinonChai);
11
+ import { generateMap } from './generate-map';
12
+ import fs from 'fs';
13
+ import { ComponentImport } from '@sitecore-content-sdk/content/tools';
14
+
15
+ describe('generateMap', () => {
16
+ const sandbox = sinon.createSandbox();
17
+
18
+ describe('generateMap', () => {
19
+ const fakePackages: ComponentImport[] = [
20
+ {
21
+ importName: 'MyLib',
22
+ importInfo: {
23
+ importFrom: '@my/lib',
24
+ },
25
+ },
26
+ {
27
+ importName: 'OtherLib',
28
+ importInfo: {
29
+ importFrom: '@other/lib',
30
+ },
31
+ },
32
+ ];
33
+
34
+ beforeEach(() => {
35
+ sandbox.stub(fs, 'writeFileSync');
36
+ });
37
+
38
+ afterEach(() => {
39
+ sandbox.restore();
40
+ });
41
+
42
+ it('should write componentMap.ts file with components from "paths" parameter', () => {
43
+ const paths = ['src/tests/test-components/generate-map'];
44
+ generateMap({ paths });
45
+
46
+ expect(fs.writeFileSync).to.have.been.calledOnce;
47
+ const [dest, content] = (fs.writeFileSync as sinon.SinonStub).getCall(0).args;
48
+ expect(dest).to.equal(path.join(process.cwd(), '.sitecore', 'component-map.ts'));
49
+
50
+ expect(content).to.include(
51
+ "import type { AstroContentSdkComponent } from '@exdst-sitecore-content-sdk/astro';"
52
+ );
53
+
54
+ expect(content).to.include(
55
+ "import Button from 'src/tests/test-components/generate-map/Button.astro';"
56
+ );
57
+ expect(content).to.include(
58
+ "import Link from 'src/tests/test-components/generate-map/Link.astro';"
59
+ );
60
+
61
+ expect(content).to.include('new Map');
62
+ expect(content).to.include("['Button', Button]");
63
+ expect(content).to.include("['Link', Link]");
64
+ expect(content).to.include('export default componentMap;');
65
+ });
66
+
67
+ it('should use template from custom componentMap function, when provided', () => {
68
+ const paths = ['src/tests/test-components/generate-map'];
69
+ const customTemplate = sinon.stub().returns('// custom template output');
70
+ const fakePackages = [
71
+ {
72
+ importName: 'CustomLib',
73
+ importInfo: {
74
+ importFrom: '@custom/lib',
75
+ },
76
+ },
77
+ ];
78
+ generateMap({ paths, componentImports: fakePackages, mapTemplate: customTemplate });
79
+
80
+ expect(customTemplate).to.have.been.calledOnce;
81
+ const [componentsArg, packagesArg] = customTemplate.getCall(0).args;
82
+ expect(packagesArg).to.deep.equal(fakePackages);
83
+ expect(componentsArg.map((c: any) => c.componentName)).to.include.members(['Button', 'Link']);
84
+
85
+ expect(fs.writeFileSync).to.have.been.calledOnce;
86
+ const [, content] = (fs.writeFileSync as sinon.SinonStub).getCall(0).args;
87
+ expect(content).to.equal('// custom template output');
88
+ });
89
+
90
+ it('should generate an empty component map if no components are found', () => {
91
+ const paths = ['src/tests/test-components/generate-map-empty'];
92
+ generateMap({ paths });
93
+
94
+ expect(fs.writeFileSync).to.have.been.calledOnce;
95
+ const [, content] = (fs.writeFileSync as sinon.SinonStub).getCall(0).args;
96
+ expect(content).to.include(
97
+ 'export const componentMap = new Map<string, AstroContentSdkComponent>(['
98
+ );
99
+ expect(content).to.not.match(/\['Button'[\s\S]*Button/);
100
+ expect(content).to.not.match(/\['Link'[\s\S]*Link/);
101
+ expect(content).to.include('export default componentMap;');
102
+ });
103
+
104
+ it('should handle multiple paths and merge their components', () => {
105
+ const paths = [
106
+ 'src/tests/test-components/generate-map',
107
+ 'src/tests/test-components/map-components',
108
+ ];
109
+
110
+ generateMap({ paths });
111
+
112
+ expect(fs.writeFileSync).to.have.been.calledOnce;
113
+ const [mainDest, content] = (fs.writeFileSync as sinon.SinonStub).getCall(0).args;
114
+ expect(mainDest).to.equal(path.join(process.cwd(), '.sitecore', 'component-map.ts'));
115
+
116
+ expect(content).to.include('new Map');
117
+
118
+ expect(content).to.include(
119
+ "import Button from 'src/tests/test-components/generate-map/Button.astro';"
120
+ );
121
+ expect(content).to.include(
122
+ "import Link from 'src/tests/test-components/generate-map/Link.astro';"
123
+ );
124
+ expect(content).to.include(
125
+ "import Bar from 'src/tests/test-components/map-components/Bar.astro';"
126
+ );
127
+
128
+ expect(content).to.include("['Button', Button]");
129
+ expect(content).to.include("['Link', Link]");
130
+ expect(content).to.include("['Bar', Bar]");
131
+ expect(content).to.include('export default componentMap;');
132
+ });
133
+
134
+ it('should not fail if packages is undefined', () => {
135
+ const paths = ['src/components'];
136
+ expect(() => generateMap({ paths, componentImports: undefined })).to.not.throw();
137
+ expect(fs.writeFileSync).to.have.been.calledOnce;
138
+ });
139
+
140
+ it('should write componentMap.ts file with components from "paths" and "packages" parameters, when provided', () => {
141
+ const paths = ['src/components'];
142
+ generateMap({ paths, componentImports: fakePackages });
143
+
144
+ expect(fs.writeFileSync).to.have.been.calledOnce;
145
+ const [, content] = (fs.writeFileSync as sinon.SinonStub).getCall(0).args;
146
+ expect(content).to.include("import MyLib from '@my/lib';");
147
+ expect(content).to.include("import OtherLib from '@other/lib';");
148
+
149
+ expect(content).to.include(
150
+ 'export const componentMap = new Map<string, AstroContentSdkComponent>(['
151
+ );
152
+ expect(content).to.include("['MyLib', MyLib],");
153
+ expect(content).to.include("['OtherLib', OtherLib],");
154
+ });
155
+
156
+ it('should use custom destination when provided', () => {
157
+ const paths = ['src/components'];
158
+ const customDir = path.join(process.cwd(), 'custom/path');
159
+ const mainDest = path.join(customDir, 'component-map.ts');
160
+
161
+ generateMap({ paths, destination: 'custom/path', includeVariants: false });
162
+
163
+ expect(fs.writeFileSync).to.have.been.calledOnce;
164
+
165
+ const encodingArg = sinon.match.string.or(sinon.match.has('encoding'));
166
+
167
+ expect(fs.writeFileSync).to.have.been.calledWithMatch(
168
+ mainDest,
169
+ sinon.match.string,
170
+ encodingArg
171
+ );
172
+ });
173
+
174
+ it('should pass exclude param into component collection call', () => {
175
+ const paths = ['src/tests/test-components/generate-map'];
176
+ const exclude = ['src/tests/test-components/generate-map/Button.*'];
177
+ generateMap({ paths, exclude });
178
+
179
+ expect(fs.writeFileSync).to.have.been.calledOnce;
180
+ const [, content] = (fs.writeFileSync as sinon.SinonStub).getCall(0).args;
181
+
182
+ expect(content).to.include(
183
+ "import Link from 'src/tests/test-components/generate-map/Link.astro';"
184
+ );
185
+ expect(content).to.not.include('Button');
186
+ });
187
+
188
+ it('should throw error when destination cannot be written to', async () => {
189
+ (fs.writeFileSync as sinon.SinonStub).throws(new Error('Disk full'));
190
+ const paths = ['src/components'];
191
+ let errorCaught = null;
192
+ try {
193
+ generateMap({ paths });
194
+ } catch (err) {
195
+ errorCaught = err;
196
+ }
197
+ expect(errorCaught).to.be.an('error');
198
+ expect((errorCaught as Error).message).to.equal('Disk full');
199
+ });
200
+ });
201
+ });
@@ -36,40 +36,28 @@ export const generateMap: GenerateMapFunction = ({
36
36
  };
37
37
 
38
38
  const buildAstroMapContent: ComponentMapTemplate = (components, componentImports): string => {
39
- const wildcardImports: string[] = [];
40
- const namedImports: string[] = [];
41
-
39
+ const componentImportsList: string[] = [];
42
40
  const componentMapEntries: string[] = [];
43
41
 
44
42
  components.forEach((component) => {
45
- wildcardImports.push(`import ${component.moduleName} from '${component.importPath}.astro';`);
43
+ componentImportsList.push(
44
+ `import ${component.moduleName} from '${component.importPath}.astro';`
45
+ );
46
46
  componentMapEntries.push(`['${component.moduleName}', ${component.moduleName}]`);
47
47
  });
48
48
 
49
49
  componentImports?.forEach((packageEntry) => {
50
- if (packageEntry.importInfo.namedImports) {
51
- namedImports.push(
52
- `import { ${packageEntry.importInfo.namedImports.join(', ')} } from '${
53
- packageEntry.importInfo.importFrom
54
- }.astro';`
55
- );
56
- packageEntry.importInfo.namedImports.forEach((importName) => {
57
- componentMapEntries.push(`['${importName}', ${importName}]`);
58
- });
59
- } else {
60
- wildcardImports.push(
61
- `import ${packageEntry.importName} from '${packageEntry.importInfo.importFrom}';`
62
- );
63
- componentMapEntries.push(`['${packageEntry.importName}', ${packageEntry.importName}]`);
64
- }
50
+ componentImportsList.push(
51
+ `import ${packageEntry.importName} from '${packageEntry.importInfo.importFrom}';`
52
+ );
53
+ componentMapEntries.push(`['${packageEntry.importName}', ${packageEntry.importName}]`);
65
54
  });
66
55
 
67
56
  return `
68
57
  import type { AstroContentSdkComponent } from '@exdst-sitecore-content-sdk/astro';
69
58
 
70
59
  // Components imported from the app itself
71
- ${wildcardImports.join('\n')}
72
- ${namedImports.join('\n')}
60
+ ${componentImportsList.join('\n')}
73
61
 
74
62
  // Components must be registered within the map to match the string key with component name in Sitecore
75
63
  export const componentMap = new Map<string, AstroContentSdkComponent>([
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable no-unused-expressions */
2
2
  import { expect, use } from 'chai';
3
3
  import spies from 'chai-spies';
4
- import { addClassName, getEditingSecret } from './utils';
4
+ import { addClassName, extractPath, getEditingSecret, removeLanguageFromPath } from './utils';
5
5
 
6
6
  use(spies);
7
7
 
@@ -34,15 +34,78 @@ describe('addClassName', () => {
34
34
  expect(modifiableAttrs).to.deep.equal({
35
35
  className: 'first-class second-class',
36
36
  });
37
+ });
38
+
39
+ it('should convert class attribute value to className when className is absent', () => {
40
+ const modifiableAttrs = {
41
+ class: 'second-class',
42
+ };
43
+ addClassName(modifiableAttrs);
44
+ expect(modifiableAttrs).to.deep.equal({
45
+ className: 'second-class',
46
+ });
47
+ });
37
48
 
38
- it('should convert class attribute value to className when className is absent', () => {
39
- const modifiableAttrs = {
40
- class: 'second-class',
41
- };
42
- addClassName(modifiableAttrs);
43
- expect(modifiableAttrs).to.deep.equal({
44
- className: 'second-class',
45
- });
49
+ it('should keep attributes unchanged when class is absent', () => {
50
+ const modifiableAttrs = {
51
+ className: 'first-class',
52
+ id: 'image',
53
+ };
54
+ addClassName(modifiableAttrs);
55
+ expect(modifiableAttrs).to.deep.equal({
56
+ className: 'first-class',
57
+ id: 'image',
46
58
  });
47
59
  });
48
60
  });
61
+
62
+ describe('extractPath', () => {
63
+ it('should return root slash when params are undefined', () => {
64
+ const result = extractPath(undefined as unknown as Record<string, string | undefined>);
65
+ expect(result).to.equal('/');
66
+ });
67
+
68
+ it('should join path segments when path is an array', () => {
69
+ const result = extractPath({ path: ['about', 'team'] as unknown as string });
70
+ expect(result).to.equal('about/team');
71
+ });
72
+
73
+ it('should return path when path is a string', () => {
74
+ const result = extractPath({ path: 'about' });
75
+ expect(result).to.equal('about');
76
+ });
77
+
78
+ it('should return root slash when path is missing', () => {
79
+ const result = extractPath({});
80
+ expect(result).to.equal('/');
81
+ });
82
+ });
83
+
84
+ describe('removeLanguageFromPath', () => {
85
+ const languages = ['en', 'fr', 'de-DE'];
86
+
87
+ it('should remove a leading language segment', () => {
88
+ const result = removeLanguageFromPath('en/About', languages);
89
+ expect(result).to.equal('About');
90
+ });
91
+
92
+ it('should remove a language segment following a _site_ segment', () => {
93
+ const result = removeLanguageFromPath('_site_Basic/en/About', languages);
94
+ expect(result).to.equal('_site_Basic/About');
95
+ });
96
+
97
+ it('should match languages case-insensitively', () => {
98
+ const result = removeLanguageFromPath('_site_Basic/FR/About', languages);
99
+ expect(result).to.equal('_site_Basic/About');
100
+ });
101
+
102
+ it('should return root slash when path only contains a language segment', () => {
103
+ const result = removeLanguageFromPath('en', languages);
104
+ expect(result).to.equal('/');
105
+ });
106
+
107
+ it('should leave path unchanged when no supported language is found', () => {
108
+ const result = removeLanguageFromPath('es/About', languages);
109
+ expect(result).to.equal('es/About');
110
+ });
111
+ });
@@ -1,183 +0,0 @@
1
- // import chalk from 'chalk';
2
- import {
3
- LayoutServiceData,
4
- ComponentRendering,
5
- PlaceholdersData,
6
- } from '@sitecore-content-sdk/content/layout';
7
- import {
8
- AstroContentSdkComponent,
9
- ComponentMap,
10
- ComponentPropsCollection,
11
- } from '../sharedTypes/component-props';
12
-
13
- export type FetchComponentPropsArguments = {
14
- layoutData: LayoutServiceData;
15
- // context: NextContext;
16
- components: ComponentMap<AstroContentSdkComponent>;
17
- };
18
-
19
- export type ComponentPropsRequest = {
20
- // fetch: ComponentPropsFetchFunction;
21
- layoutData: LayoutServiceData;
22
- rendering: ComponentRendering;
23
- // context: NextContext;
24
- };
25
-
26
- /**
27
- * The service for fetching component props.
28
- * @public
29
- */
30
- export class ComponentPropsService {
31
- async fetchComponentProps(
32
- params: FetchComponentPropsArguments
33
- ): Promise<ComponentPropsCollection> {
34
- const { layoutData, components } = params;
35
- const requests = await this.collectRequests({
36
- placeholders: layoutData.sitecore.route?.placeholders,
37
- components,
38
- layoutData,
39
- // context,
40
- });
41
- return await this.execRequests(requests);
42
- }
43
-
44
- /**
45
- * Go through layout service data, check all renderings using displayName, which should make some side effects.
46
- * Write result in requests variable
47
- * @param {object} params params
48
- * @param {PlaceholdersData} [params.placeholders]
49
- * @param {ComponentMap} params.components
50
- * @param {LayoutServiceData} params.layoutData
51
- * @param {ComponentPropsRequest[]} params.requests
52
- * @returns {ComponentPropsRequest[]} array of requests
53
- */
54
- protected async collectRequests(params: {
55
- placeholders?: PlaceholdersData;
56
- components: ComponentMap<AstroContentSdkComponent>;
57
- layoutData: LayoutServiceData;
58
- // context: NextContext;
59
- requests?: ComponentPropsRequest[];
60
- }): Promise<ComponentPropsRequest[]> {
61
- const { placeholders = {}, layoutData } = params;
62
-
63
- // Will be called on first round
64
- if (!params.requests) {
65
- params.requests = [];
66
- }
67
-
68
- const renderings = this.flatRenderings(placeholders);
69
-
70
- const actions = renderings.map(async (r) => {
71
- // const fetchFunc = (await this.getModule(components, r.componentName))
72
- // ?.getComponentServerProps;
73
-
74
- const fetchFunc = ''; // getModule
75
-
76
- if (fetchFunc) {
77
- params.requests &&
78
- params.requests.push({
79
- // fetch: fetchFunc,
80
- rendering: r,
81
- layoutData: layoutData,
82
- // context,
83
- });
84
- }
85
-
86
- // If placeholders exist in current rendering
87
- if (r.placeholders) {
88
- await this.collectRequests({
89
- ...params,
90
- placeholders: r.placeholders,
91
- });
92
- }
93
- });
94
-
95
- await Promise.all(actions);
96
-
97
- return params.requests;
98
- }
99
-
100
- /**
101
- * Execute request for component props
102
- * @param {ComponentPropsRequest[]} requests requests
103
- * @returns {Promise<ComponentPropsCollection>} requests result
104
- */
105
- protected async execRequests(
106
- requests: ComponentPropsRequest[]
107
- ): Promise<ComponentPropsCollection> {
108
- const componentProps: ComponentPropsCollection = {};
109
-
110
- const promises = requests.map((req) => {
111
- const { uid } = req.rendering;
112
-
113
- if (!uid) {
114
- console.log(
115
- `Component ${req.rendering.componentName} doesn't have uid, can't store data for this component`
116
- );
117
- return;
118
- }
119
-
120
- // return req
121
- // .fetch(req.rendering, req.layoutData /*, req.context*/)
122
- // .then((result) => {
123
- // // Set component specific data in componentProps store
124
- // componentProps[uid] = result;
125
- // })
126
- // .catch((error) => {
127
- // const errLog = `Error during preload data for component ${
128
- // req.rendering.componentName
129
- // } (${uid}): ${error.message || error}`;
130
-
131
- // console.error(chalk.red(errLog));
132
-
133
- // componentProps[uid] = {
134
- // error: error.message || errLog,
135
- // componentName: req.rendering.componentName,
136
- // };
137
- // });
138
- });
139
-
140
- await Promise.all(promises);
141
-
142
- return componentProps;
143
- }
144
-
145
- /**
146
- * Take renderings from all placeholders and returns a flat array of renderings.
147
- * @example
148
- * const placeholders = {
149
- * x1: [{ uid: 1 }, { uid: 2 }],
150
- * x2: [{ uid: 11 }, { uid: 22 }]
151
- * }
152
- *
153
- * flatRenderings(placeholders);
154
- *
155
- * RESULT: [{ uid: 1 }, { uid: 2 }, { uid: 11 }, { uid: 22 }]
156
- * @param {PlaceholdersData} placeholders placeholders
157
- * @returns {ComponentRendering[]} renderings
158
- */
159
- protected flatRenderings(placeholders: PlaceholdersData): ComponentRendering[] {
160
- const allComponentRenderings: ComponentRendering[] = [];
161
- const placeholdersArr = Object.values(placeholders);
162
-
163
- placeholdersArr.forEach((pl) => {
164
- const renderings = pl as ComponentRendering[];
165
- allComponentRenderings.push(...renderings);
166
- });
167
-
168
- return allComponentRenderings;
169
- }
170
-
171
- // private async getModule(
172
- // components: ComponentMap<AstroContentSdkComponent>,
173
- // componentName: string
174
- // ) {
175
- // const component = components.get(componentName);
176
-
177
- // if (!component) return null;
178
-
179
- // //const module = component.dynamicModule ? await component?.dynamicModule?.() : component;
180
- // const module = component;
181
- // return module as AstroContentSdkComponent;
182
- // }
183
- }