From ef9314e5a4244232cfff9350d9c6e3dbb57a6115 Mon Sep 17 00:00:00 2001 From: Anshul Goyal Date: Thu, 6 Aug 2020 17:23:03 +0530 Subject: [PATCH] feat(v2): update swizzle command to suggest component/theme (#3021) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../docusaurus-theme-classic/src/index.js | 12 + packages/docusaurus-types/src/index.d.ts | 1 + packages/docusaurus/bin/docusaurus.js | 6 +- packages/docusaurus/package.json | 1 + packages/docusaurus/src/commands/swizzle.ts | 289 +++++++++++++++--- website/docs/cli.md | 14 +- website/docs/lifecycle-apis.md | 18 ++ 7 files changed, 291 insertions(+), 50 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/index.js b/packages/docusaurus-theme-classic/src/index.js index 8d0653830b..f659f6532e 100644 --- a/packages/docusaurus-theme-classic/src/index.js +++ b/packages/docusaurus-theme-classic/src/index.js @@ -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; diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 80aef41cdb..14ee8dd2b9 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -157,6 +157,7 @@ export interface Plugin { preBodyTags?: HtmlTags; postBodyTags?: HtmlTags; }; + getSwizzleComponentList?(): string[]; } export type ConfigureWebpackFn = Plugin['configureWebpack']; diff --git a/packages/docusaurus/bin/docusaurus.js b/packages/docusaurus/bin/docusaurus.js index e19917b31c..e997c0a88a 100755 --- a/packages/docusaurus/bin/docusaurus.js +++ b/packages/docusaurus/bin/docusaurus.js @@ -102,18 +102,20 @@ cli }); cli - .command('swizzle [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, ); }); diff --git a/packages/docusaurus/package.json b/packages/docusaurus/package.json index 0e9d3bb755..12a3e0561d 100644 --- a/packages/docusaurus/package.json +++ b/packages/docusaurus/package.json @@ -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", diff --git a/packages/docusaurus/src/commands/swizzle.ts b/packages/docusaurus/src/commands/swizzle.ts index f133ab547a..dd1b64db8a 100644 --- a/packages/docusaurus/src/commands/swizzle.ts +++ b/packages/docusaurus/src/commands/swizzle.ts @@ -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 { + let results: Array = []; + 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 { + // 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, + 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 { + // 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 { - const plugin = importFresh(themeName) as ( - context: LoadContext, - ) => Plugin; 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 does not exist, we try to swizzle .(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; + } 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 does not exist, we try to swizzle .(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))); + } } } diff --git a/website/docs/cli.md b/website/docs/cli.md index b72e165bf9..4b7aed9c3b 100644 --- a/website/docs/cli.md +++ b/website/docs/cli.md @@ -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 [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 ` 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. diff --git a/website/docs/lifecycle-apis.md b/website/docs/lifecycle-apis.md index 11cdda37c1..3d39ad44f4 100644 --- a/website/docs/lifecycle-apis.md +++ b/website/docs/lifecycle-apis.md @@ -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.