mirror of
https://github.com/facebook/docusaurus.git
synced 2025-12-26 01:33:02 +00:00
fix: allow swizzling a component's parent folder (#7225)
This commit is contained in:
parent
f7c995b15a
commit
c3add31ebf
3
packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp1.css
generated
Normal file
3
packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp1.css
generated
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.testClass {
|
||||
background: black;
|
||||
}
|
||||
5
packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp1.tsx
generated
Normal file
5
packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp1.tsx
generated
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function NoIndexComp1() {
|
||||
return <div>NoIndexComp1</div>;
|
||||
}
|
||||
5
packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp2.tsx
generated
Normal file
5
packages/docusaurus/src/commands/swizzle/__tests__/__fixtures__/theme/NoIndex/NoIndexComp2.tsx
generated
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function NoIndexComp2() {
|
||||
return <div>NoIndexComp2</div>;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function NoIndexSubComp() {
|
||||
return <div>NoIndexSubComp</div>;
|
||||
}
|
||||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue