From c3add31ebfd042c7d2ed7931c3df695545ff49bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Fri, 22 Apr 2022 14:23:33 +0200 Subject: [PATCH] fix: allow swizzling a component's parent folder (#7225) --- .../theme/NoIndex/NoIndexComp1.css | 3 + .../theme/NoIndex/NoIndexComp1.tsx | 5 ++ .../theme/NoIndex/NoIndexComp2.tsx | 5 ++ .../NoIndex/NoIndexSub/NoIndexSubComp.tsx | 5 ++ .../swizzle/__tests__/components.test.ts | 41 ++++++++++++ .../commands/swizzle/__tests__/testUtils.ts | 5 ++ .../src/commands/swizzle/components.ts | 65 +++++++++++++++++-- .../docusaurus/src/commands/swizzle/config.ts | 42 ++++++------ 8 files changed, 144 insertions(+), 27 deletions(-) create mode 100644 packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp1.css create mode 100644 packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp1.tsx create mode 100644 packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp2.tsx create mode 100644 packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexSub/NoIndexSubComp.tsx diff --git a/packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp1.css b/packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp1.css new file mode 100644 index 0000000000..7aa192f3b3 --- /dev/null +++ b/packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp1.css @@ -0,0 +1,3 @@ +.testClass { + background: black; +} diff --git a/packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp1.tsx b/packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp1.tsx new file mode 100644 index 0000000000..dcf3086a30 --- /dev/null +++ b/packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp1.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function NoIndexComp1() { + return
NoIndexComp1
; +} diff --git a/packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp2.tsx b/packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp2.tsx new file mode 100644 index 0000000000..f070a38c03 --- /dev/null +++ b/packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp2.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function NoIndexComp2() { + return
NoIndexComp2
; +} diff --git a/packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexSub/NoIndexSubComp.tsx b/packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexSub/NoIndexSubComp.tsx new file mode 100644 index 0000000000..fdb3570a32 --- /dev/null +++ b/packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexSub/NoIndexSubComp.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function NoIndexSubComp() { + return
NoIndexSubComp
; +} diff --git a/packages/docusaurus/src/commands/swizzle/__tests__/components.test.ts b/packages/docusaurus/src/commands/swizzle/__tests__/components.test.ts index d278e727aa..eb0b284457 100644 --- a/packages/docusaurus/src/commands/swizzle/__tests__/components.test.ts +++ b/packages/docusaurus/src/commands/swizzle/__tests__/components.test.ts @@ -19,6 +19,9 @@ describe('readComponentNames', () => { Components.ComponentInSubFolder, Components.Sibling, Components.FirstLevelComponent, + Components.NoIndexComp1, + Components.NoIndexComp2, + Components.NoIndexSubComp, ]); }); }); @@ -66,6 +69,11 @@ describe('getThemeComponents', () => { Components.ComponentInSubFolder, Components.Sibling, Components.FirstLevelComponent, + Components.NoIndex, + Components.NoIndexComp1, + Components.NoIndexComp2, + Components.NoIndexSub, + Components.NoIndexSubComp, ]); }); @@ -155,6 +163,39 @@ describe('getThemeComponents', () => { expect( themeComponents.getActionStatus(Components.FirstLevelComponent, 'eject'), ).toBe('unsafe'); + + expect( + themeComponents.getActionStatus(Components.NoIndexComp1, 'wrap'), + ).toBe('unsafe'); + expect( + themeComponents.getActionStatus(Components.NoIndexComp1, 'eject'), + ).toBe('unsafe'); + expect( + themeComponents.getActionStatus(Components.NoIndexComp2, 'wrap'), + ).toBe('unsafe'); + expect( + themeComponents.getActionStatus(Components.NoIndexComp2, 'eject'), + ).toBe('unsafe'); + expect( + themeComponents.getActionStatus(Components.NoIndexSubComp, 'wrap'), + ).toBe('unsafe'); + expect( + themeComponents.getActionStatus(Components.NoIndexSubComp, 'eject'), + ).toBe('unsafe'); + + // Intermediate folders are not real components: forbidden to wrap! + expect(themeComponents.getActionStatus(Components.NoIndex, 'wrap')).toBe( + 'forbidden', + ); + expect(themeComponents.getActionStatus(Components.NoIndex, 'eject')).toBe( + 'unsafe', + ); + expect(themeComponents.getActionStatus(Components.NoIndexSub, 'wrap')).toBe( + 'forbidden', + ); + expect( + themeComponents.getActionStatus(Components.NoIndexSub, 'eject'), + ).toBe('unsafe'); }); it('isSafeAction', async () => { diff --git a/packages/docusaurus/src/commands/swizzle/__tests__/testUtils.ts b/packages/docusaurus/src/commands/swizzle/__tests__/testUtils.ts index 97e0aba8a7..e0e9acf795 100644 --- a/packages/docusaurus/src/commands/swizzle/__tests__/testUtils.ts +++ b/packages/docusaurus/src/commands/swizzle/__tests__/testUtils.ts @@ -16,6 +16,11 @@ export const Components = { Sibling: 'ComponentInFolder/Sibling', ComponentInFolder: 'ComponentInFolder', FirstLevelComponent: 'FirstLevelComponent', + NoIndex: 'NoIndex', + NoIndexComp1: 'NoIndex/NoIndexComp1', + NoIndexComp2: 'NoIndex/NoIndexComp2', + NoIndexSub: 'NoIndex/NoIndexSub', + NoIndexSubComp: 'NoIndex/NoIndexSub/NoIndexSubComp', }; export async function createTempSiteDir(): Promise { diff --git a/packages/docusaurus/src/commands/swizzle/components.ts b/packages/docusaurus/src/commands/swizzle/components.ts index 33968ad0fa..f9dc0d7beb 100644 --- a/packages/docusaurus/src/commands/swizzle/components.ts +++ b/packages/docusaurus/src/commands/swizzle/components.ts @@ -38,6 +38,35 @@ export type ThemeComponents = { const formatComponentName = (componentName: string): string => componentName.replace(/[/\\]index\.[jt]sx?/, '').replace(/\.[jt]sx?/, ''); +function sortComponentNames(componentNames: string[]): string[] { + return componentNames.sort(); // Algo may change? +} + +/** + * Expand a list of components to include and return parent folders. + * If a folder is not directly a component (no Folder/index.tsx file), + * we still want to be able to swizzle --eject that folder. + * See https://github.com/facebook/docusaurus/pull/7175#issuecomment-1103757218 + * + * @param componentNames the original list of component names + */ +function getMissingIntermediateComponentFolderNames( + componentNames: string[], +): string[] { + function getAllIntermediatePaths(componentName: string): string[] { + const paths = componentName.split('/'); + return _.range(1, paths.length + 1).map((i) => paths.slice(0, i).join('/')); + } + + const expandedComponentNames = _.uniq( + componentNames.flatMap((componentName) => + getAllIntermediatePaths(componentName), + ), + ); + + return _.difference(expandedComponentNames, componentNames); +} + const skipReadDirNames = ['__test__', '__tests__', '__mocks__', '__fixtures__']; export async function readComponentNames(themePath: string): Promise { @@ -88,13 +117,9 @@ export async function readComponentNames(themePath: string): Promise { const componentFiles = await walk(themePath); - const componentFilesOrdered = _.orderBy( - componentFiles, - [(f) => f.componentName], - ['asc'], - ); + const componentNames = componentFiles.map((f) => f.componentName); - return componentFilesOrdered.map((f) => f.componentName); + return sortComponentNames(componentNames); } export function listComponentNames(themeComponents: ThemeComponents): string { @@ -125,8 +150,25 @@ export async function getThemeComponents({ }, description: FallbackSwizzleComponentDescription, }; + const FallbackIntermediateFolderSwizzleComponentConfig: SwizzleComponentConfig = + { + actions: { + // It doesn't make sense to wrap an intermediate folder + // because it has not any index component + wrap: 'forbidden', + eject: FallbackSwizzleActionStatus, + }, + description: FallbackSwizzleComponentDescription, + }; - const allComponents = await readComponentNames(themePath); + const allInitialComponents = await readComponentNames(themePath); + + const missingIntermediateComponentFolderNames = + getMissingIntermediateComponentFolderNames(allInitialComponents); + + const allComponents = sortComponentNames( + allInitialComponents.concat(missingIntermediateComponentFolderNames), + ); function getConfig(component: string): SwizzleComponentConfig { if (!allComponents.includes(component)) { @@ -134,6 +176,15 @@ export async function getThemeComponents({ `Can't get component config: component doesn't exist: ${component}`, ); } + const config = swizzleConfig.components[component]; + if (config) { + return config; + } + const isIntermediateFolder = + missingIntermediateComponentFolderNames.includes(component); + if (isIntermediateFolder) { + return FallbackIntermediateFolderSwizzleComponentConfig; + } return ( swizzleConfig.components[component] ?? FallbackSwizzleComponentConfig ); diff --git a/packages/docusaurus/src/commands/swizzle/config.ts b/packages/docusaurus/src/commands/swizzle/config.ts index d7e4ffb7fb..a16c7dfd4a 100644 --- a/packages/docusaurus/src/commands/swizzle/config.ts +++ b/packages/docusaurus/src/commands/swizzle/config.ts @@ -48,33 +48,35 @@ function getModuleSwizzleConfig( return undefined; } -export function normalizeSwizzleConfig( - unsafeSwizzleConfig: unknown, -): SwizzleConfig { - const schema = Joi.object({ - components: Joi.object() - .pattern( - Joi.string(), - Joi.object({ - actions: Joi.object().pattern( - Joi.string().valid(...SwizzleActions), - Joi.string().valid(...SwizzleActionsStatuses), - ), - description: Joi.string(), - }), - ) - .required(), - }); - - const result = schema.validate(unsafeSwizzleConfig); +const SwizzleConfigSchema = Joi.object({ + components: Joi.object() + .pattern( + Joi.string(), + Joi.object({ + actions: Joi.object().pattern( + Joi.string().valid(...SwizzleActions), + Joi.string().valid(...SwizzleActionsStatuses), + ), + description: Joi.string(), + }), + ) + .required(), +}); +function validateSwizzleConfig(unsafeSwizzleConfig: unknown): SwizzleConfig { + const result = SwizzleConfigSchema.validate(unsafeSwizzleConfig); if (result.error) { throw new Error( `Swizzle config does not match expected schema: ${result.error.message}`, ); } + return result.value; +} - const swizzleConfig: SwizzleConfig = result.value; +export function normalizeSwizzleConfig( + unsafeSwizzleConfig: unknown, +): SwizzleConfig { + const swizzleConfig = validateSwizzleConfig(unsafeSwizzleConfig); // Ensure all components always declare all actions Object.values(swizzleConfig.components).forEach((componentConfig) => {