From 1e048dae856c0857f6797a97a4e93d2bda3bd24d Mon Sep 17 00:00:00 2001 From: Davie Date: Tue, 15 Aug 2023 11:44:06 +0100 Subject: [PATCH 1/7] feat: expose source namespace to plugin lifecycle methods --- packages/core/src/Source.ts | 6 ++++-- packages/core/src/worker/Source.worker.ts | 6 ++++-- packages/types/src/Plugin.ts | 8 ++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/core/src/Source.ts b/packages/core/src/Source.ts index c1bb212e..77cb335c 100644 --- a/packages/core/src/Source.ts +++ b/packages/core/src/Source.ts @@ -113,7 +113,8 @@ export default class Source { pageExtensions: this.#pageExtensions, ignorePages: this.#ignorePages, serialiser: this.serialiser, - config: this.config.asReadOnly() + config: this.config.asReadOnly(), + namespace: this.namespace } ); const timeTaken = new Date().getTime() - initTime; @@ -149,7 +150,8 @@ export default class Source { pageExtensions: this.#pageExtensions, ignorePages: this.#ignorePages, serialiser: this.serialiser, - config: this.config.asReadOnly() + config: this.config.asReadOnly(), + namespace: this.namespace }); const timeTaken = new Date().getTime() - initTime; if (timeTaken > 800) { diff --git a/packages/core/src/worker/Source.worker.ts b/packages/core/src/worker/Source.worker.ts index 2d4c2b13..f126efcc 100644 --- a/packages/core/src/worker/Source.worker.ts +++ b/packages/core/src/worker/Source.worker.ts @@ -39,7 +39,8 @@ if (isMainThread) { config, serialiser, ignorePages: workerData.ignorePages, - pageExtensions: workerData.pageExtensions + pageExtensions: workerData.pageExtensions, + namespace: workerData.namespace }) ), switchMap((pages: Page[]) => @@ -58,7 +59,8 @@ if (isMainThread) { config, ignorePages: workerData.ignorePages, pageExtensions: workerData.pageExtensions, - serialiser + serialiser, + namespace: workerData.namespace }); // In the main thread we would freeze the filesystem here, but since we throw it away after sending it to the parent process, // we don't bother freezing diff --git a/packages/types/src/Plugin.ts b/packages/types/src/Plugin.ts index 949e4ed4..a1bf5927 100644 --- a/packages/types/src/Plugin.ts +++ b/packages/types/src/Plugin.ts @@ -30,6 +30,7 @@ export type Plugin< * @param pages Array of pages from the source * @param param.serialiser A matching `Serialiser` for serialising/deserialising pages when reading/writing to the filesystem * @param param.config A mutable object for sharing data with other lifecycle phases of all plugins for this source (including in the main thread) in this plugin + * @param param.namespace The namespace of the source running the plugin * @param options The options passed in when declaring the plugin * @returns {Promise} Must re-return an array of `Page` objects, modified or not */ @@ -40,6 +41,7 @@ export type Plugin< config: MutableData; pageExtensions: string[]; ignorePages: string[]; + namespace: string; }, options?: TOptions ) => Promise>; @@ -49,6 +51,7 @@ export type Plugin< * @param mutableFilesystem Mutable virtual filesystem instance with all of this source's pages inside (and symlinks applied) * @param param.serialiser A matching `Serialiser` for serialising/deserialising pages when reading/writing to the filesystem * @param param.config A mutable object for sharing data with other lifecycle phases of all plugins for this source (including in the main thread) in this plugin + * @param param.namespace The namespace of the source running the plugin * @param options The options passed in when declaring the plugin * @returns {void} No return expected */ @@ -59,6 +62,7 @@ export type Plugin< pageExtensions: string[]; ignorePages: string[]; config: MutableData; + namespace: string; }, options?: TOptions ) => Promise; @@ -76,6 +80,7 @@ export type Plugin< * @param param.globalConfig An immutable object for reading data from other lifecycle phases of all plugins. Shared across all sources. * @param param.sharedFilesystem Mutable filesystem instance independent of any sources. Useful for global pages, like sitemaps * @param param.globalFilesystem Immutable union filesystem instance with all source's pages (and symlinks applied) + * @param param.namespace The namespace of the source running the plugin * @param options The options passed in when declaring the plugin * @returns {void} No return expected */ @@ -89,6 +94,7 @@ export type Plugin< globalConfig: ImmutableData; pageExtensions: string[]; ignorePages: string[]; + namespace: string; }, options?: TOptions ) => Promise; @@ -101,6 +107,7 @@ export type Plugin< * @param param.config An immutable object for reading data from other lifecycle phases of all plugins for this source in the child process for this plugin * @param param.serialiser A matching `Serialiser` for serialising/deserialising pages when reading/writing to the filesystem * @param param.globalFilesystem Immutable union filesystem instance with all source's pages (and symlinks applied) + * @param param.namespace The namespace of the source running the plugin * @param options The options passed in when declaring the plugin * @returns {Promise} A boolean indicating whether to clear the cache for this source */ @@ -112,6 +119,7 @@ export type Plugin< globalFilesystem: IUnionVolume; pageExtensions: string[]; ignorePages: string[]; + namespace: string; }, options?: TOptions ) => Promise; From 3ad8b7129de955457191dcfc12fbdc73a8f9beba Mon Sep 17 00:00:00 2001 From: Davie Date: Tue, 15 Aug 2023 17:09:28 +0100 Subject: [PATCH 2/7] feat: apply namespace shared config to sources that don't have their own config --- packages/plugins/src/SharedConfigPlugin.ts | 122 ++++++++++++++++++--- 1 file changed, 105 insertions(+), 17 deletions(-) diff --git a/packages/plugins/src/SharedConfigPlugin.ts b/packages/plugins/src/SharedConfigPlugin.ts index 3c335a65..da349e12 100644 --- a/packages/plugins/src/SharedConfigPlugin.ts +++ b/packages/plugins/src/SharedConfigPlugin.ts @@ -43,17 +43,31 @@ export interface SharedConfigPluginOptions { * It then exports a JSON file (name: `options.filename`) into each directory with the merged config for that level */ const SharedConfigPlugin: PluginType = { - async $afterSource(pages, { ignorePages, pageExtensions }) { + async $afterSource(pages, { ignorePages, pageExtensions, config, namespace }) { const isNonHiddenPage = createPageTest(ignorePages, pageExtensions); let finalSharedConfig; - const indexPagesWithSharedConfig = pages.filter( + const indexPages = pages.filter( page => path.posix.basename(page.fullPath, path.posix.extname(page.fullPath)) === 'index' && - page.sharedConfig !== undefined && isNonHiddenPage(page.fullPath) ); + const indexPagesWithSharedConfig = indexPages.filter(page => page.sharedConfig !== undefined); + + if (indexPagesWithSharedConfig.length === 0 && indexPages.length > 0) { + const rootPath = indexPages[0].fullPath; + const applyNamespaceSharedConfig = { + [`${namespace}-${rootPath}`]: { + paths: indexPages.map(indexPage => indexPage.fullPath), + rootPath, + namespace + } + }; + config.setData({ applyNamespaceSharedConfig }); + return pages; + } + for (const page of indexPagesWithSharedConfig) { if (finalSharedConfig === undefined) { // first shared config we have found so seed the finalSharedConfig @@ -113,24 +127,98 @@ const SharedConfigPlugin: PluginType 0) { + let closestSharedConfigIndex = 0; + for (const pagePath of indexPagesWithoutConfig) { + for (let i = 0; i < sharedConfigFiles.length; i++) { + if (isWithin(sharedConfigFiles[i], pagePath)) { + closestSharedConfigIndex = i; + } } + + const sharedConfigFile = path.posix.join( + path.posix.dirname(String(pagePath)), + options.filename + ); + + const closestSharedConfig = path.posix.resolve( + path.dirname(String(pagePath)), + sharedConfigFiles[closestSharedConfigIndex] + ); + config.setAliases(closestSharedConfig, [sharedConfigFile]); } + } + }, + async afterUpdate(mutableFilesystem, { sharedFilesystem, globalConfig, namespace }, options) { + const { applyNamespaceSharedConfig } = globalConfig.data; - const sharedConfigFile = path.posix.join( - path.posix.dirname(String(pagePath)), - options.filename - ); + if (applyNamespaceSharedConfig === undefined) { + // there is no source that exists that has told us it needs to share a parent shared config + return; + } - const closestSharedConfig = path.posix.resolve( - path.dirname(String(pagePath)), - sharedConfigFiles[closestSharedConfigIndex] - ); - config.setAliases(closestSharedConfig, [sharedConfigFile]); + // find all the entries that match the namespace the plugin is running against + const namespaceSharedConfigs: { + paths: string[]; + rootPath: string; + namespace: string; + }[] = Object.keys(applyNamespaceSharedConfig) + .filter(key => { + const keyNamespace = key.split('-')?.[0]; + return keyNamespace === namespace; + }) + .map(key => applyNamespaceSharedConfig?.[key] || []); + + for (const namespaceSharedConfig of namespaceSharedConfigs) { + if (await mutableFilesystem.promises.exists(namespaceSharedConfig.rootPath)) { + // a source does need a namespace shared config but the source running this plugin is the source that needs it + // so we don't need to do anything here + continue; + } + + for (const applyPath of namespaceSharedConfig.paths) { + if (!(await sharedFilesystem.promises.exists(applyPath))) { + sharedFilesystem.promises.mkdir(path.posix.dirname(String(applyPath)), { + recursive: true + }); + } + + let parentDir = path.posix.join(path.posix.dirname(String(applyPath)), '../'); + let closestSharedConfigPath = path.posix.join(parentDir, options.filename); + + while (parentDir !== path.posix.sep) { + // walk up the directories in the path to find the closest shared config file + closestSharedConfigPath = path.posix.join(parentDir, options.filename); + if (await mutableFilesystem.promises.exists(closestSharedConfigPath)) { + break; + } + parentDir = path.posix.join(path.posix.dirname(String(closestSharedConfigPath)), '../'); + } + + const aliasSharedConfigPath = path.posix.join( + path.posix.dirname(String(applyPath)), + options.filename + ); + + if ( + (await mutableFilesystem.promises.exists(closestSharedConfigPath)) && + !(await sharedFilesystem.promises.exists(aliasSharedConfigPath)) + ) { + console.log( + `[Mosaic][Plugin] Source has no shared config. Root index page is: ${namespaceSharedConfig.rootPath}` + ); + console.log( + '[Mosaic][Plugin] Copying shared config ', + closestSharedConfigPath, + '-->', + aliasSharedConfigPath + ); + await sharedFilesystem.promises.writeFile( + aliasSharedConfigPath, + await mutableFilesystem.promises.readFile(closestSharedConfigPath) + ); + } + } } } }; From e68f94673ce8817b3410a3d0444a12a10a2a9d1b Mon Sep 17 00:00:00 2001 From: Davie Date: Thu, 17 Aug 2023 09:16:12 +0100 Subject: [PATCH 3/7] feat: namespace shared config is applied across sources If a source has no shared config of its own and shares a namespace with another source that does have a shared config, then the namespace shared config is used. --- packages/plugins/src/SharedConfigPlugin.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/plugins/src/SharedConfigPlugin.ts b/packages/plugins/src/SharedConfigPlugin.ts index da349e12..71ca178a 100644 --- a/packages/plugins/src/SharedConfigPlugin.ts +++ b/packages/plugins/src/SharedConfigPlugin.ts @@ -58,7 +58,7 @@ const SharedConfigPlugin: PluginType 0) { const rootPath = indexPages[0].fullPath; const applyNamespaceSharedConfig = { - [`${namespace}-${rootPath}`]: { + [`${namespace}~~${rootPath}`]: { paths: indexPages.map(indexPage => indexPage.fullPath), rootPath, namespace @@ -153,7 +153,7 @@ const SharedConfigPlugin: PluginType { - const keyNamespace = key.split('-')?.[0]; + const keyNamespace = key.split('~~')?.[0]; return keyNamespace === namespace; }) .map(key => applyNamespaceSharedConfig?.[key] || []); @@ -182,7 +182,6 @@ const SharedConfigPlugin: PluginType Date: Thu, 17 Aug 2023 09:41:24 +0100 Subject: [PATCH 4/7] test: update `$afterSource` tests for `SharedConfigPlugin` --- .../src/__tests__/SharedConfigPlugin.test.tsx | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/packages/plugins/src/__tests__/SharedConfigPlugin.test.tsx b/packages/plugins/src/__tests__/SharedConfigPlugin.test.tsx index 6ae47310..d1e60d69 100644 --- a/packages/plugins/src/__tests__/SharedConfigPlugin.test.tsx +++ b/packages/plugins/src/__tests__/SharedConfigPlugin.test.tsx @@ -71,6 +71,45 @@ const pages: SharedConfigPage[] = [ } ]; +const pagesWithoutSharedConfig: SharedConfigPage[] = [ + { + fullPath: '/FolderA/index.mdx', + route: 'route/folderA/index', + title: 'Folder A Index' + }, + { + fullPath: '/FolderA/pageA.mdx', + route: 'route/folderA/pageA', + title: 'Folder A Page A' + }, + { + fullPath: '/FolderA/pageB.mdx', + route: 'route/folderA/pageB', + title: 'Folder A Page B' + }, + { + fullPath: '/FolderA/SubfolderA/index.mdx', + route: 'route/folderA/subfolderA/index', + title: 'Subfolder A Index' + }, + { + fullPath: '/FolderA/SubfolderA/PageA.mdx', + route: 'route/folderA/subfolderA/pageA', + title: 'Subfolder A Page A' + }, + { + fullPath: '/FolderA/SubfolderB/index.mdx', + route: 'route/folderA/subfolderB/index', + title: 'Subfolder B Index' + }, + { + fullPath: '/FolderA/SubfolderB/SubfolderC/SubfolderD/index.mdx', + route: 'route/folderA/subfolderB/subfolderC/subfolderD/index', + title: 'Subfolder D Index' + } +]; + +let setDataMock = jest.fn(); let setAliasesMock = jest.fn(); let setRefMock = jest.fn(); let writeFileMock = jest.fn(); @@ -103,6 +142,10 @@ describe('GIVEN the SharedConfigPlugin', () => { expect(SharedConfigPlugin).toHaveProperty('$beforeSend'); }); + test('THEN it should use the `afterUpdate` lifecycle event', () => { + expect(SharedConfigPlugin).toHaveProperty('afterUpdate'); + }); + describe('WHEN `$afterSource` is called', () => { let updatedPages: SharedConfigPage[] = []; beforeEach(async () => { @@ -135,6 +178,105 @@ describe('GIVEN the SharedConfigPlugin', () => { }); }); + describe('WHEN `$afterSource` is called and *No* page has a shared config', () => { + const testNamespace = 'test-ns'; + beforeEach(async () => { + const $afterSource = SharedConfigPlugin.$afterSource; + // @ts-ignore + + (await $afterSource?.( + pagesWithoutSharedConfig, + { + pageExtensions: ['.mdx'], + ignorePages: ['shared-config.json'], + config: { + setData: setDataMock + }, + namespace: testNamespace + }, + { filename: 'shared-config.json' } + )) || []; + }); + + afterEach(() => { + setDataMock.mockReset(); + }); + + test('THEN applyNamespaceSharedConfig property is written to the plugin config object', () => { + expect(setDataMock).toHaveBeenCalledTimes(1); + + const namespaceConfig = setDataMock.mock.calls[0][0]; + + expect(namespaceConfig).toEqual({ + applyNamespaceSharedConfig: { + 'test-ns-/FolderA/index.mdx': { + namespace: 'test-ns', + paths: [ + '/FolderA/index.mdx', + '/FolderA/SubfolderA/index.mdx', + '/FolderA/SubfolderB/index.mdx', + '/FolderA/SubfolderB/SubfolderC/SubfolderD/index.mdx' + ], + rootPath: '/FolderA/index.mdx' + } + } + }); + }); + + test('THEN applyNamespaceSharedConfig property has a key that is a combination of namespace and first index page path', () => { + expect(setDataMock).toHaveBeenCalledTimes(1); + + const namespaceConfig = setDataMock.mock.calls[0][0]; + + expect( + namespaceConfig.applyNamespaceSharedConfig['test-ns-/FolderA/index.mdx'] + ).toBeDefined(); + + expect( + namespaceConfig.applyNamespaceSharedConfig['test-ns-/FolderA/index.mdx'].namespace + ).toEqual(testNamespace); + }); + + test('THEN the namespace is included in the namespace shared config object', () => { + expect(setDataMock).toHaveBeenCalledTimes(1); + + const namespaceConfig = setDataMock.mock.calls[0][0]; + + expect( + namespaceConfig.applyNamespaceSharedConfig['test-ns-/FolderA/index.mdx'].namespace + ).toEqual(testNamespace); + }); + + test('THEN the root path is included in the namespace shared config object', () => { + expect(setDataMock).toHaveBeenCalledTimes(1); + + const namespaceConfig = setDataMock.mock.calls[0][0]; + + expect( + namespaceConfig.applyNamespaceSharedConfig['test-ns-/FolderA/index.mdx'].rootPath + ).toEqual('/FolderA/index.mdx'); + }); + + test('THEN all index pages in the source are included in namespace shared config object', () => { + expect(setDataMock).toHaveBeenCalledTimes(1); + + const namespaceConfig = setDataMock.mock.calls[0][0]; + + expect( + namespaceConfig.applyNamespaceSharedConfig['test-ns-/FolderA/index.mdx'].paths.length + ).toEqual(4); + + expect( + namespaceConfig.applyNamespaceSharedConfig['test-ns-/FolderA/index.mdx'].paths + ).toEqual([ + '/FolderA/index.mdx', + '/FolderA/SubfolderA/index.mdx', + '/FolderA/SubfolderB/index.mdx', + '/FolderA/SubfolderB/SubfolderC/SubfolderD/index.mdx' + ]); + }); + }); + describe('WHEN `$beforeSend` is called', () => { beforeEach(async () => { const $beforeSend = SharedConfigPlugin.$beforeSend; From 5ad6393aaca9baf8eae8c73668127ee3838649cb Mon Sep 17 00:00:00 2001 From: Davie Date: Thu, 17 Aug 2023 12:12:07 +0100 Subject: [PATCH 5/7] test: add test for `afterUpdate` in `SharedConfigPlugin` --- .../src/__tests__/SharedConfigPlugin.test.tsx | 200 +++++++++++++++++- 1 file changed, 192 insertions(+), 8 deletions(-) diff --git a/packages/plugins/src/__tests__/SharedConfigPlugin.test.tsx b/packages/plugins/src/__tests__/SharedConfigPlugin.test.tsx index d1e60d69..d2c2070d 100644 --- a/packages/plugins/src/__tests__/SharedConfigPlugin.test.tsx +++ b/packages/plugins/src/__tests__/SharedConfigPlugin.test.tsx @@ -115,7 +115,7 @@ let setRefMock = jest.fn(); let writeFileMock = jest.fn(); const volume = { promises: { - exists: jest.fn().mockResolvedValue(true), + exists: jest.fn(), glob: jest.fn().mockResolvedValue([ pages[0].fullPath, // Folder A Index pages[3].fullPath, // Subfolder A Index @@ -209,7 +209,7 @@ describe('GIVEN the SharedConfigPlugin', () => { expect(namespaceConfig).toEqual({ applyNamespaceSharedConfig: { - 'test-ns-/FolderA/index.mdx': { + 'test-ns~~/FolderA/index.mdx': { namespace: 'test-ns', paths: [ '/FolderA/index.mdx', @@ -229,11 +229,11 @@ describe('GIVEN the SharedConfigPlugin', () => { const namespaceConfig = setDataMock.mock.calls[0][0]; expect( - namespaceConfig.applyNamespaceSharedConfig['test-ns-/FolderA/index.mdx'] + namespaceConfig.applyNamespaceSharedConfig['test-ns~~/FolderA/index.mdx'] ).toBeDefined(); expect( - namespaceConfig.applyNamespaceSharedConfig['test-ns-/FolderA/index.mdx'].namespace + namespaceConfig.applyNamespaceSharedConfig['test-ns~~/FolderA/index.mdx'].namespace ).toEqual(testNamespace); }); @@ -243,7 +243,7 @@ describe('GIVEN the SharedConfigPlugin', () => { const namespaceConfig = setDataMock.mock.calls[0][0]; expect( - namespaceConfig.applyNamespaceSharedConfig['test-ns-/FolderA/index.mdx'].namespace + namespaceConfig.applyNamespaceSharedConfig['test-ns~~/FolderA/index.mdx'].namespace ).toEqual(testNamespace); }); @@ -253,7 +253,7 @@ describe('GIVEN the SharedConfigPlugin', () => { const namespaceConfig = setDataMock.mock.calls[0][0]; expect( - namespaceConfig.applyNamespaceSharedConfig['test-ns-/FolderA/index.mdx'].rootPath + namespaceConfig.applyNamespaceSharedConfig['test-ns~~/FolderA/index.mdx'].rootPath ).toEqual('/FolderA/index.mdx'); }); @@ -263,11 +263,11 @@ describe('GIVEN the SharedConfigPlugin', () => { const namespaceConfig = setDataMock.mock.calls[0][0]; expect( - namespaceConfig.applyNamespaceSharedConfig['test-ns-/FolderA/index.mdx'].paths.length + namespaceConfig.applyNamespaceSharedConfig['test-ns~~/FolderA/index.mdx'].paths.length ).toEqual(4); expect( - namespaceConfig.applyNamespaceSharedConfig['test-ns-/FolderA/index.mdx'].paths + namespaceConfig.applyNamespaceSharedConfig['test-ns~~/FolderA/index.mdx'].paths ).toEqual([ '/FolderA/index.mdx', '/FolderA/SubfolderA/index.mdx', @@ -334,4 +334,188 @@ describe('GIVEN the SharedConfigPlugin', () => { ); }); }); + + describe('WHEN `afterUpdate` is called', () => { + describe('AND WHEN there is **NO** namespace shared config to apply at all', () => { + beforeEach(async () => { + const afterUpdate = SharedConfigPlugin.afterUpdate; + // @ts-ignore + (await afterUpdate?.( + volume, + { + config: { + setRef: setRefMock, + setAliases: setAliasesMock + }, + serialiser: { + deserialise: jest.fn().mockImplementation((_path, value) => Promise.resolve(value)) + }, + globalConfig: { data: {} }, + ignorePages: [], + pageExtensions: [] + }, + { filename: 'test-shared-config.json' } + )) || []; + }); + + test('THEN no action is taken', () => { + expect(volume.promises.exists).not.toBeCalled(); + }); + }); + + describe('AND WHEN there is **NO** namespace shared config to apply for the namespace', () => { + beforeEach(async () => { + const afterUpdate = SharedConfigPlugin.afterUpdate; + // @ts-ignore + (await afterUpdate?.( + volume, + { + config: { + setRef: setRefMock, + setAliases: setAliasesMock + }, + serialiser: { + deserialise: jest.fn().mockImplementation((_path, value) => Promise.resolve(value)) + }, + globalConfig: { + data: { applyNamespaceSharedConfig: { ['another-ns']: { data: 'data' } } } + }, + ignorePages: [], + pageExtensions: [], + namespace: 'test-ns' + }, + { filename: 'test-shared-config.json' } + )) || []; + }); + + test('THEN no action is taken', () => { + expect(volume.promises.exists).not.toBeCalled(); + }); + }); + + describe('AND WHEN there is namespace shared config to apply for the namespace BUT the rootPath is part of this source', () => { + beforeEach(async () => { + const afterUpdate = SharedConfigPlugin.afterUpdate; + + volume.promises.exists.mockResolvedValue(true); + + // @ts-ignore + (await afterUpdate?.( + volume, + { + config: { + setRef: setRefMock, + setAliases: setAliasesMock + }, + serialiser: { + deserialise: jest.fn().mockImplementation((_path, value) => Promise.resolve(value)) + }, + globalConfig: { + data: { + applyNamespaceSharedConfig: { + 'test-ns~~/FolderY/index.mdx': { + namespace: 'test-ns', + paths: ['/FolderY/index.mdx', '/FolderY/SubfolderX/index.mdx'], + rootPath: '/FolderY/index.mdx' + } + } + } + }, + ignorePages: [], + pageExtensions: [], + namespace: 'test-ns' + }, + { filename: 'test-shared-config.json' } + )) || []; + }); + + afterEach(() => { + volume.promises.exists.mockReset(); + }); + + test('THEN we check for existence of the root path', () => { + expect(volume.promises.exists).toHaveBeenCalledTimes(1); + expect(volume.promises.exists.mock.calls[0][0]).toEqual('/FolderY/index.mdx'); + }); + + test('THEN we do *NOT* write out any new shared config file', () => { + expect(volume.promises.exists).toHaveBeenCalledTimes(1); + expect(volume.promises.exists.mock.calls[0][0]).toEqual('/FolderY/index.mdx'); + }); + }); + + describe('AND WHEN there is namespace shared config to apply for the namespace AND the rootPath is **NOT** part of this source', () => { + const sharedFilesystem = { + promises: { + exists: jest.fn(), + mkdir: jest.fn(), + readdir: jest.fn(), + readFile: jest.fn(), + realpath: jest.fn(), + stat: jest.fn(), + symlink: jest.fn(), + unlink: jest.fn(), + writeFile: jest.fn() + } + }; + beforeEach(async () => { + const afterUpdate = SharedConfigPlugin.afterUpdate; + volume.promises.readFile.mockReset(); + volume.promises.exists.mockResolvedValueOnce(false).mockResolvedValue(true); + sharedFilesystem.promises.exists.mockResolvedValue(false); + + // @ts-ignore + (await afterUpdate?.( + volume, + { + sharedFilesystem, + config: { + setRef: setRefMock, + setAliases: setAliasesMock + }, + serialiser: { + deserialise: jest.fn().mockImplementation((_path, value) => Promise.resolve(value)) + }, + globalConfig: { + data: { + applyNamespaceSharedConfig: { + 'test-ns~~/FolderY/index.mdx': { + namespace: 'test-ns', + paths: ['/FolderY/index.mdx'], + rootPath: '/FolderY/index.mdx' + } + } + } + }, + ignorePages: [], + pageExtensions: [], + namespace: 'test-ns' + }, + { filename: 'test-shared-config.json' } + )) || []; + }); + + afterEach(() => { + volume.promises.exists.mockReset(); + sharedFilesystem.promises.exists.mockReset(); + volume.promises.readFile.mockReset(); + }); + + test('THEN we check the shared config json file exists in the mutable fs and the target shared config is not in the shared filesystem', () => { + expect(sharedFilesystem.promises.exists.mock.calls[0][0]).toEqual('/FolderY/index.mdx'); + expect(sharedFilesystem.promises.mkdir.mock.calls[0][0]).toEqual('/FolderY'); + + console.log('HELLO', volume.promises.exists.mock.calls); + + expect(volume.promises.exists.mock.calls[1][0]).toEqual('/test-shared-config.json'); + + expect(sharedFilesystem.promises.writeFile).toBeCalledTimes(1); + expect(sharedFilesystem.promises.writeFile.mock.calls[0][0]).toEqual( + '/FolderY/test-shared-config.json' + ); + expect(volume.promises.readFile).toBeCalledTimes(1); + expect(volume.promises.readFile.mock.calls[0][0]).toEqual('/test-shared-config.json'); + }); + }); + }); }); From e9bbdfbabcc2c6318f764166a3fc5ebcac351a30 Mon Sep 17 00:00:00 2001 From: Davie Date: Thu, 17 Aug 2023 12:32:05 +0100 Subject: [PATCH 6/7] chore: add changeset --- .changeset/eleven-paws-invite.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/eleven-paws-invite.md diff --git a/.changeset/eleven-paws-invite.md b/.changeset/eleven-paws-invite.md new file mode 100644 index 00000000..71e6042a --- /dev/null +++ b/.changeset/eleven-paws-invite.md @@ -0,0 +1,8 @@ +--- +'@jpmorganchase/mosaic-core': patch +'@jpmorganchase/mosaic-plugins': patch +'@jpmorganchase/mosaic-site': patch +'@jpmorganchase/mosaic-types': patch +--- + +`SharedConfigPlugin` can now apply a shared config to a source that doesn't have one but shares a namespace with 1 that does. From 5ddf54988f3543279476dcd6dc34ab59aafa7a87 Mon Sep 17 00:00:00 2001 From: Davie Date: Thu, 17 Aug 2023 13:25:53 +0100 Subject: [PATCH 7/7] feat: use unique id for namespace shared configs --- packages/plugins/src/SharedConfigPlugin.ts | 10 +++--- .../src/__tests__/SharedConfigPlugin.test.tsx | 36 ++++++++----------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/packages/plugins/src/SharedConfigPlugin.ts b/packages/plugins/src/SharedConfigPlugin.ts index 71ca178a..4cc9891c 100644 --- a/packages/plugins/src/SharedConfigPlugin.ts +++ b/packages/plugins/src/SharedConfigPlugin.ts @@ -1,6 +1,7 @@ +import crypto from 'node:crypto'; +import path from 'node:path'; import type { Page, Plugin as PluginType } from '@jpmorganchase/mosaic-types'; import { flatten, escapeRegExp } from 'lodash-es'; -import path from 'path'; import deepmerge from 'deepmerge'; function createPageTest(ignorePages, pageExtensions) { @@ -58,7 +59,7 @@ const SharedConfigPlugin: PluginType 0) { const rootPath = indexPages[0].fullPath; const applyNamespaceSharedConfig = { - [`${namespace}~~${rootPath}`]: { + [`${crypto.randomUUID()}`]: { paths: indexPages.map(indexPage => indexPage.fullPath), rootPath, namespace @@ -163,10 +164,7 @@ const SharedConfigPlugin: PluginType { - const keyNamespace = key.split('~~')?.[0]; - return keyNamespace === namespace; - }) + .filter(key => applyNamespaceSharedConfig[key].namespace === namespace) .map(key => applyNamespaceSharedConfig?.[key] || []); for (const namespaceSharedConfig of namespaceSharedConfigs) { diff --git a/packages/plugins/src/__tests__/SharedConfigPlugin.test.tsx b/packages/plugins/src/__tests__/SharedConfigPlugin.test.tsx index d2c2070d..97d00d49 100644 --- a/packages/plugins/src/__tests__/SharedConfigPlugin.test.tsx +++ b/packages/plugins/src/__tests__/SharedConfigPlugin.test.tsx @@ -1,6 +1,12 @@ import { Page } from '@jpmorganchase/mosaic-types'; import SharedConfigPlugin from '../SharedConfigPlugin'; +jest.mock('node:crypto', () => { + return { + randomUUID: jest.fn(() => '123') + }; +}); + type SharedConfigPage = Page & { sharedConfig?: any }; /** * @@ -209,7 +215,7 @@ describe('GIVEN the SharedConfigPlugin', () => { expect(namespaceConfig).toEqual({ applyNamespaceSharedConfig: { - 'test-ns~~/FolderA/index.mdx': { + '123': { namespace: 'test-ns', paths: [ '/FolderA/index.mdx', @@ -228,13 +234,9 @@ describe('GIVEN the SharedConfigPlugin', () => { const namespaceConfig = setDataMock.mock.calls[0][0]; - expect( - namespaceConfig.applyNamespaceSharedConfig['test-ns~~/FolderA/index.mdx'] - ).toBeDefined(); + expect(namespaceConfig.applyNamespaceSharedConfig['123']).toBeDefined(); - expect( - namespaceConfig.applyNamespaceSharedConfig['test-ns~~/FolderA/index.mdx'].namespace - ).toEqual(testNamespace); + expect(namespaceConfig.applyNamespaceSharedConfig['123'].namespace).toEqual(testNamespace); }); test('THEN the namespace is included in the namespace shared config object', () => { @@ -242,9 +244,7 @@ describe('GIVEN the SharedConfigPlugin', () => { const namespaceConfig = setDataMock.mock.calls[0][0]; - expect( - namespaceConfig.applyNamespaceSharedConfig['test-ns~~/FolderA/index.mdx'].namespace - ).toEqual(testNamespace); + expect(namespaceConfig.applyNamespaceSharedConfig['123'].namespace).toEqual(testNamespace); }); test('THEN the root path is included in the namespace shared config object', () => { @@ -252,9 +252,9 @@ describe('GIVEN the SharedConfigPlugin', () => { const namespaceConfig = setDataMock.mock.calls[0][0]; - expect( - namespaceConfig.applyNamespaceSharedConfig['test-ns~~/FolderA/index.mdx'].rootPath - ).toEqual('/FolderA/index.mdx'); + expect(namespaceConfig.applyNamespaceSharedConfig['123'].rootPath).toEqual( + '/FolderA/index.mdx' + ); }); test('THEN all index pages in the source are included in namespace shared config object', () => { @@ -262,13 +262,9 @@ describe('GIVEN the SharedConfigPlugin', () => { const namespaceConfig = setDataMock.mock.calls[0][0]; - expect( - namespaceConfig.applyNamespaceSharedConfig['test-ns~~/FolderA/index.mdx'].paths.length - ).toEqual(4); + expect(namespaceConfig.applyNamespaceSharedConfig['123'].paths.length).toEqual(4); - expect( - namespaceConfig.applyNamespaceSharedConfig['test-ns~~/FolderA/index.mdx'].paths - ).toEqual([ + expect(namespaceConfig.applyNamespaceSharedConfig['123'].paths).toEqual([ '/FolderA/index.mdx', '/FolderA/SubfolderA/index.mdx', '/FolderA/SubfolderB/index.mdx', @@ -505,8 +501,6 @@ describe('GIVEN the SharedConfigPlugin', () => { expect(sharedFilesystem.promises.exists.mock.calls[0][0]).toEqual('/FolderY/index.mdx'); expect(sharedFilesystem.promises.mkdir.mock.calls[0][0]).toEqual('/FolderY'); - console.log('HELLO', volume.promises.exists.mock.calls); - expect(volume.promises.exists.mock.calls[1][0]).toEqual('/test-shared-config.json'); expect(sharedFilesystem.promises.writeFile).toBeCalledTimes(1);