mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-01 10:22:30 +02:00
feat(v2): update swizzle command to suggest component/theme (#3021)
* update swizzle command * change messages * fix formatting * add docs * fix component lisiting * allow themes to provide a list for safe swizzle components * support both commanjs and ES exports * fix debug plugin doesn't swizzle * add dos * remove unsed file * fix docs plugin not swizzle properly * fix console.log * use new validate * fix linting * use config options for plugin * rerun test * fix type * add a comment * Update packages/docusaurus/src/commands/swizzle.ts * Update packages/docusaurus/src/commands/swizzle.ts Co-authored-by: Sébastien Lorber <slorber@users.noreply.github.com>
This commit is contained in:
parent
f234c407f1
commit
ef9314e5a4
7 changed files with 291 additions and 50 deletions
|
@ -120,4 +120,16 @@ module.exports = function (context, options) {
|
|||
};
|
||||
};
|
||||
|
||||
const swizzleAllowedComponents = [
|
||||
'CodeBlock',
|
||||
'DocSidebar',
|
||||
'Footer',
|
||||
'NotFound',
|
||||
'SearchBar',
|
||||
'hooks/useTheme',
|
||||
'prism-include-languages',
|
||||
];
|
||||
|
||||
module.exports.getSwizzleComponentList = () => swizzleAllowedComponents;
|
||||
|
||||
module.exports.validateThemeConfig = validateThemeConfig;
|
||||
|
|
1
packages/docusaurus-types/src/index.d.ts
vendored
1
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -157,6 +157,7 @@ export interface Plugin<T, U = unknown> {
|
|||
preBodyTags?: HtmlTags;
|
||||
postBodyTags?: HtmlTags;
|
||||
};
|
||||
getSwizzleComponentList?(): string[];
|
||||
}
|
||||
|
||||
export type ConfigureWebpackFn = Plugin<unknown>['configureWebpack'];
|
||||
|
|
|
@ -102,18 +102,20 @@ cli
|
|||
});
|
||||
|
||||
cli
|
||||
.command('swizzle <themeName> [componentName] [siteDir]')
|
||||
.command('swizzle [themeName] [componentName] [siteDir]')
|
||||
.description('Copy the theme files into website folder for customization.')
|
||||
.option(
|
||||
'--typescript',
|
||||
'Copy TypeScript theme files when possible (default: false)',
|
||||
)
|
||||
.action((themeName, componentName, siteDir = '.', {typescript}) => {
|
||||
.option('--danger', 'Enable swizzle for internal component of themes')
|
||||
.action((themeName, componentName, siteDir = '.', {typescript, danger}) => {
|
||||
wrapCommand(swizzle)(
|
||||
path.resolve(siteDir),
|
||||
themeName,
|
||||
componentName,
|
||||
typescript,
|
||||
danger,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -74,6 +74,7 @@
|
|||
"import-fresh": "^3.2.1",
|
||||
"inquirer": "^7.2.0",
|
||||
"is-root": "^2.1.0",
|
||||
"leven": "^3.1.0",
|
||||
"lodash": "^4.5.2",
|
||||
"lodash.flatmap": "^4.5.0",
|
||||
"lodash.has": "^4.5.2",
|
||||
|
|
|
@ -9,61 +9,264 @@ import chalk = require('chalk');
|
|||
import fs from 'fs-extra';
|
||||
import importFresh from 'import-fresh';
|
||||
import path from 'path';
|
||||
import {Plugin, LoadContext} from '@docusaurus/types';
|
||||
import {Plugin, LoadContext, PluginConfig} from '@docusaurus/types';
|
||||
import leven from 'leven';
|
||||
|
||||
import {THEME_PATH} from '../constants';
|
||||
import {loadContext} from '../server';
|
||||
import {loadContext, loadPluginConfigs} from '../server';
|
||||
import initPlugins from '../server/plugins/init';
|
||||
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||
|
||||
export function getPluginNames(plugins: PluginConfig[]): string[] {
|
||||
return plugins.map((plugin) => {
|
||||
const pluginPath = Array.isArray(plugin) ? plugin[0] : plugin;
|
||||
let packagePath = path.dirname(pluginPath);
|
||||
while (packagePath) {
|
||||
if (fs.existsSync(path.join(packagePath, 'package.json'))) {
|
||||
break;
|
||||
} else {
|
||||
packagePath = path.dirname(packagePath);
|
||||
}
|
||||
}
|
||||
if (packagePath === '.') {
|
||||
return pluginPath;
|
||||
}
|
||||
return (importFresh(path.join(packagePath, 'package.json')) as {
|
||||
name: string;
|
||||
}).name as string;
|
||||
});
|
||||
}
|
||||
|
||||
function walk(dir: string): Array<string> {
|
||||
let results: Array<string> = [];
|
||||
const list = fs.readdirSync(dir);
|
||||
list.forEach((file: string) => {
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat && stat.isDirectory()) {
|
||||
results = results.concat(walk(fullPath));
|
||||
} else if (!/node_modules|.css|.d.ts|.d.map/.test(fullPath)) {
|
||||
results.push(fullPath);
|
||||
}
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
function readComponent(themePath: string) {
|
||||
return walk(themePath).map((filePath) =>
|
||||
path
|
||||
.relative(themePath, filePath)
|
||||
.replace(/(\/|\\)index.(js|tsx|ts|jsx)/, '')
|
||||
.replace(/.(js|tsx|ts|jsx)/, ''),
|
||||
);
|
||||
}
|
||||
|
||||
// load components from theme based on configurations
|
||||
function getComponentName(
|
||||
themePath: string,
|
||||
plugin: any,
|
||||
danger: boolean,
|
||||
): Array<string> {
|
||||
// support both commonjs and ES style exports
|
||||
const getSwizzleComponentList =
|
||||
plugin.default?.getSwizzleComponentList ?? plugin.getSwizzleComponentList;
|
||||
if (getSwizzleComponentList) {
|
||||
const allowedComponent = getSwizzleComponentList();
|
||||
if (danger) {
|
||||
return readComponent(themePath);
|
||||
}
|
||||
return allowedComponent;
|
||||
}
|
||||
return readComponent(themePath);
|
||||
}
|
||||
|
||||
function themeComponents(
|
||||
themePath: string,
|
||||
plugin: Plugin<unknown>,
|
||||
danger: boolean,
|
||||
): string {
|
||||
const components = colorCode(themePath, plugin, danger);
|
||||
return `Theme Components available for swizzle:\n${components.join('\n')}`;
|
||||
}
|
||||
|
||||
function formatedThemeNames(themeNames: string[]): string {
|
||||
return `Themes available for swizzle:\n${themeNames.join('\n')}`;
|
||||
}
|
||||
|
||||
function colorCode(
|
||||
themePath: string,
|
||||
plugin: any,
|
||||
danger: boolean,
|
||||
): Array<string> {
|
||||
// support both commonjs and ES style exports
|
||||
const getSwizzleComponentList =
|
||||
plugin.default?.getSwizzleComponentList ?? plugin.getSwizzleComponentList;
|
||||
if (getSwizzleComponentList) {
|
||||
const allowedComponent = getSwizzleComponentList();
|
||||
if (danger) {
|
||||
const components = readComponent(themePath);
|
||||
const componentMap = allowedComponent.reduce(
|
||||
(acc: {[key: string]: boolean}, component) => {
|
||||
acc[component] = true;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const colorCodedComponent = components
|
||||
.filter((component) => !componentMap[component])
|
||||
.map((component) => chalk.red(component));
|
||||
return [
|
||||
...allowedComponent.map((component) => chalk.green(component)),
|
||||
...colorCodedComponent,
|
||||
];
|
||||
}
|
||||
return allowedComponent;
|
||||
}
|
||||
return readComponent(themePath);
|
||||
}
|
||||
|
||||
export default async function swizzle(
|
||||
siteDir: string,
|
||||
themeName: string,
|
||||
themeName?: string,
|
||||
componentName?: string,
|
||||
typescript?: boolean,
|
||||
danger?: boolean,
|
||||
): Promise<void> {
|
||||
const plugin = importFresh(themeName) as (
|
||||
context: LoadContext,
|
||||
) => Plugin<unknown>;
|
||||
const context = loadContext(siteDir);
|
||||
const pluginInstance = plugin(context);
|
||||
let fromPath = typescript
|
||||
? pluginInstance.getTypeScriptThemePath?.()
|
||||
: pluginInstance.getThemePath?.();
|
||||
|
||||
if (fromPath) {
|
||||
let toPath = path.resolve(siteDir, THEME_PATH);
|
||||
if (componentName) {
|
||||
fromPath = path.join(fromPath, componentName);
|
||||
toPath = path.join(toPath, componentName);
|
||||
|
||||
// Handle single TypeScript/JavaScript file only.
|
||||
// E.g: if <fromPath> does not exist, we try to swizzle <fromPath>.(ts|tsx|js) instead
|
||||
if (!fs.existsSync(fromPath)) {
|
||||
if (fs.existsSync(`${fromPath}.ts`)) {
|
||||
[fromPath, toPath] = [`${fromPath}.ts`, `${toPath}.ts`];
|
||||
} else if (fs.existsSync(`${fromPath}.tsx`)) {
|
||||
[fromPath, toPath] = [`${fromPath}.tsx`, `${toPath}.tsx`];
|
||||
} else if (fs.existsSync(`${fromPath}.js`)) {
|
||||
[fromPath, toPath] = [`${fromPath}.js`, `${toPath}.js`];
|
||||
const pluginConfigs = loadPluginConfigs(context);
|
||||
const pluginNames = getPluginNames(pluginConfigs);
|
||||
const plugins = initPlugins({
|
||||
pluginConfigs,
|
||||
context,
|
||||
});
|
||||
const themeNames = pluginNames.filter((_, index) =>
|
||||
typescript
|
||||
? plugins[index].getTypeScriptThemePath
|
||||
: plugins[index].getThemePath,
|
||||
);
|
||||
if (!themeName) {
|
||||
console.log(formatedThemeNames(themeNames));
|
||||
} else {
|
||||
let pluginModule;
|
||||
try {
|
||||
pluginModule = importFresh(themeName) as (
|
||||
context: LoadContext,
|
||||
) => Plugin<unknown>;
|
||||
} catch {
|
||||
let suggestion;
|
||||
themeNames.forEach((name) => {
|
||||
if (leven(name, themeName) < 4) {
|
||||
suggestion = name;
|
||||
}
|
||||
});
|
||||
throw new Error(
|
||||
`Theme ${themeName} not found. ${
|
||||
suggestion
|
||||
? `Did you mean "${suggestion}" ?`
|
||||
: formatedThemeNames(themeNames)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
const plugin = pluginModule.default ?? pluginModule;
|
||||
const validateOptions =
|
||||
pluginModule.default?.validateOptions ?? pluginModule.validateOptions;
|
||||
let pluginOptions;
|
||||
const resolvedThemeName = require.resolve(themeName);
|
||||
// find the plugin from list of plugin and get options if specified
|
||||
pluginConfigs.forEach((pluginConfig) => {
|
||||
// plugin can be a [string], [string,object] or string.
|
||||
if (Array.isArray(pluginConfig)) {
|
||||
if (require.resolve(pluginConfig[0]) === resolvedThemeName) {
|
||||
if (pluginConfig.length === 2) {
|
||||
const [, options] = pluginConfig;
|
||||
pluginOptions = options;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (validateOptions) {
|
||||
// normilize options
|
||||
const normalizedOptions = validateOptions({
|
||||
validate: normalizePluginOptions,
|
||||
options: pluginOptions,
|
||||
});
|
||||
pluginOptions = normalizedOptions;
|
||||
}
|
||||
await fs.copy(fromPath, toPath);
|
||||
const pluginInstance = plugin(context, pluginOptions);
|
||||
const themePath = typescript
|
||||
? pluginInstance.getTypeScriptThemePath?.()
|
||||
: pluginInstance.getThemePath?.();
|
||||
if (componentName) {
|
||||
let fromPath = themePath;
|
||||
if (fromPath) {
|
||||
let toPath = path.resolve(siteDir, THEME_PATH);
|
||||
fromPath = path.join(fromPath, componentName);
|
||||
toPath = path.join(toPath, componentName);
|
||||
const components = getComponentName(
|
||||
themePath,
|
||||
pluginModule,
|
||||
Boolean(danger),
|
||||
);
|
||||
|
||||
const relativeDir = path.relative(process.cwd(), toPath);
|
||||
const fromMsg = chalk.blue(
|
||||
componentName ? `${themeName} ${chalk.yellow(componentName)}` : themeName,
|
||||
);
|
||||
const toMsg = chalk.cyan(relativeDir);
|
||||
console.log(
|
||||
`\n${chalk.green('Success!')} Copied ${fromMsg} to ${toMsg}.\n`,
|
||||
);
|
||||
} else if (typescript) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`${themeName} does not provide TypeScript theme code via getTypeScriptThemePath().`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.warn(chalk.yellow(`${themeName} does not provide any theme code.`));
|
||||
// Handle single TypeScript/JavaScript file only.
|
||||
// E.g: if <fromPath> does not exist, we try to swizzle <fromPath>.(ts|tsx|js) instead
|
||||
if (!fs.existsSync(fromPath)) {
|
||||
if (fs.existsSync(`${fromPath}.ts`)) {
|
||||
[fromPath, toPath] = [`${fromPath}.ts`, `${toPath}.ts`];
|
||||
} else if (fs.existsSync(`${fromPath}.tsx`)) {
|
||||
[fromPath, toPath] = [`${fromPath}.tsx`, `${toPath}.tsx`];
|
||||
} else if (fs.existsSync(`${fromPath}.js`)) {
|
||||
[fromPath, toPath] = [`${fromPath}.js`, `${toPath}.js`];
|
||||
} else {
|
||||
let suggestion;
|
||||
components.forEach((name) => {
|
||||
if (leven(name, componentName) < 3) {
|
||||
suggestion = name;
|
||||
}
|
||||
});
|
||||
throw new Error(
|
||||
`Component ${componentName} not found.${
|
||||
suggestion
|
||||
? ` Did you mean "${suggestion}"?`
|
||||
: `${themeComponents(
|
||||
themePath,
|
||||
pluginModule,
|
||||
Boolean(danger),
|
||||
)}`
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!components.includes(componentName) && !danger) {
|
||||
throw new Error(
|
||||
`${componentName} is an internal component, if you want to swizzle it use "--danger" flag.`,
|
||||
);
|
||||
}
|
||||
await fs.copy(fromPath, toPath);
|
||||
|
||||
const relativeDir = path.relative(process.cwd(), toPath);
|
||||
const fromMsg = chalk.blue(
|
||||
componentName
|
||||
? `${themeName} ${chalk.yellow(componentName)}`
|
||||
: themeName,
|
||||
);
|
||||
const toMsg = chalk.cyan(relativeDir);
|
||||
console.log(
|
||||
`\n${chalk.green('Success!')} Copied ${fromMsg} to ${toMsg}.\n`,
|
||||
);
|
||||
} else if (typescript) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`${themeName} does not provide TypeScript theme code via getTypeScriptThemePath().`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
chalk.yellow(`${themeName} does not provide any theme code.`),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(themeComponents(themePath, pluginModule, Boolean(danger)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ We highly discourage swizzling of components until we've reached a Beta stage. T
|
|||
Change any Docusaurus theme components to your liking with `docusaurus swizzle`.
|
||||
|
||||
```shell
|
||||
docusaurus swizzle <themeName> [componentName] [siteDir]
|
||||
docusaurus swizzle [themeName] [componentName] [siteDir]
|
||||
|
||||
# Example (leaving out the siteDir to indicate this directory)
|
||||
docusaurus swizzle @docusaurus/theme-classic DocSidebar
|
||||
|
@ -87,12 +87,16 @@ docusaurus swizzle @docusaurus/theme-classic DocSidebar
|
|||
|
||||
Running the command will copy the relevant theme files to your site folder. You may then make any changes to it and Docusaurus will use it instead of the one provided from the theme.
|
||||
|
||||
`docusaurus swizzle` without `themeName` lists all the themes available for swizzling similarly `docusaurus swizzle <themeName>` without `componentName` lists all the components available for swizzling.
|
||||
|
||||
#### Options
|
||||
|
||||
| Name | Description |
|
||||
| ------------------ | ------------------------------------- |
|
||||
| `themeName` | The name of the theme you are using. |
|
||||
| `swizzleComponent` | The name of the component to swizzle. |
|
||||
| Name | Description |
|
||||
| ------------------ | ---------------------------------------|
|
||||
| `themeName` | The name of the theme you are using. |
|
||||
| `swizzleComponent` | The name of the component to swizzle. |
|
||||
| `--danger` | Allow swizzling of unstable components |
|
||||
| `--typescript` | Swizzle typescript components |
|
||||
|
||||
To unswizzle a component, simply delete the files of the swizzled component.
|
||||
|
||||
|
|
|
@ -509,6 +509,24 @@ module.exports = function (context, options) {
|
|||
};
|
||||
```
|
||||
|
||||
## `getSwizzleComponentList()`
|
||||
|
||||
Return a list of stable component that are considered as safe for swizzling. These components will be listed in swizzle component without `--danger`. All the components are considers unstable by default. If an empty array is returned then all components are considered unstable, if `undefined` is returned then all component are considered stable.
|
||||
|
||||
```js {0-12} title="my-theme/src/index.js"
|
||||
const swizzleAllowedComponents = [
|
||||
'CodeBlock',
|
||||
'DocSidebar',
|
||||
'Footer',
|
||||
'NotFound',
|
||||
'SearchBar',
|
||||
'hooks/useTheme',
|
||||
'prism-include-languages',
|
||||
];
|
||||
|
||||
module.exports.getSwizzleComponentList = () => swizzleAllowedComponents;
|
||||
```
|
||||
|
||||
## `getClientModules()`
|
||||
|
||||
Returns an array of paths to the modules that are to be imported in the client bundle. These modules are imported globally before React even renders the initial UI.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue