fix: allow swizzling a component's parent folder (#7225)

This commit is contained in:
Sébastien Lorber 2022-04-22 14:23:33 +02:00 committed by GitHub
parent f7c995b15a
commit c3add31ebf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 144 additions and 27 deletions

View File

@ -0,0 +1,3 @@
.testClass {
background: black;
}

View File

@ -0,0 +1,5 @@
import React from 'react';
export default function NoIndexComp1() {
return <div>NoIndexComp1</div>;
}

View File

@ -0,0 +1,5 @@
import React from 'react';
export default function NoIndexComp2() {
return <div>NoIndexComp2</div>;
}

View File

@ -0,0 +1,5 @@
import React from 'react';
export default function NoIndexSubComp() {
return <div>NoIndexSubComp</div>;
}

View File

@ -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 () => {

View File

@ -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<string> {

View File

@ -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<string[]> {
@ -88,13 +117,9 @@ export async function readComponentNames(themePath: string): Promise<string[]> {
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
);

View File

@ -48,33 +48,35 @@ function getModuleSwizzleConfig(
return undefined;
}
export function normalizeSwizzleConfig(
unsafeSwizzleConfig: unknown,
): SwizzleConfig {
const schema = Joi.object<SwizzleConfig>({
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<SwizzleConfig>({
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) => {