diff --git a/docs/.vuepress/configs/sidebar/en.ts b/docs/.vuepress/configs/sidebar/en.ts index ce2d714a37..80e5c68110 100644 --- a/docs/.vuepress/configs/sidebar/en.ts +++ b/docs/.vuepress/configs/sidebar/en.ts @@ -82,6 +82,7 @@ export const en: SidebarConfig = { '/reference/plugin/google-analytics.md', '/reference/plugin/medium-zoom.md', '/reference/plugin/nprogress.md', + '/reference/plugin/register-components.md', ], }, { diff --git a/docs/.vuepress/configs/sidebar/zh.ts b/docs/.vuepress/configs/sidebar/zh.ts index 957966c476..0c68797211 100644 --- a/docs/.vuepress/configs/sidebar/zh.ts +++ b/docs/.vuepress/configs/sidebar/zh.ts @@ -85,6 +85,7 @@ export const zh: SidebarConfig = { '/zh/reference/plugin/google-analytics.md', '/zh/reference/plugin/medium-zoom.md', '/zh/reference/plugin/nprogress.md', + '/zh/reference/plugin/register-components.md', ], }, { diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 7d6e1965bd..e7673a40cc 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -164,7 +164,7 @@ The arguments of the function are changed, too. Files in this directory will not be registered as Vue components automatically. -You need to register your components manually in `.vuepress/clientAppEnhance.{js,ts}`. +You need to use `@vuepress/plugin-register-components`, or register your components manually in `.vuepress/clientAppEnhance.{js,ts}`. #### .vuepress/theme/ @@ -268,7 +268,7 @@ Some major breaking changes: - There is no **conventional theme directory structure** anymore. - The file `theme/enhanceApp.js` or `theme/clientAppEnhance.{js,ts}` will not be used as client app enhance file implicitly. You need to specify it explicitly in `clientAppEnhanceFiles` hook. - - Files in `theme/global-components/` directory will not be registered as Vue components automatically. You need to register components manually in `clientAppEnhance.{js,ts}`. + - Files in `theme/global-components/` directory will not be registered as Vue components automatically. You need to use `@vuepress/plugin-register-components`, or register components manually in `clientAppEnhance.{js,ts}`. - Files in `theme/layouts/` directory will not be registered as layout components automatically. You need to specify it explicitly in `layouts` option. - Files in `theme/templates/` directory will not be used as dev / ssr template automatically. - Always provide a theme entry file, and do not use `"main": "layouts/Layout.vue"` as the theme entry. diff --git a/docs/reference/plugin/register-components.md b/docs/reference/plugin/register-components.md new file mode 100644 index 0000000000..34687a5899 --- /dev/null +++ b/docs/reference/plugin/register-components.md @@ -0,0 +1,115 @@ +# register-components + +> [@vuepress/plugin-register-components](https://www.npmjs.com/package/@vuepress/plugin-register-components) + +Register Vue components from component files or directory automatically. + +## Options + +### components + +- Type: `Record` + +- Default: `{}` + +- Details: + + An object that defines name of components and their corresponding file path. + + The key will be used as the component name, and the value is an absolute path of the component file. + + If the component name from this option conflicts with [componentsDir](#componentsdir) option, this option will have a higher priority. + +- Example: + +```js +module.exports = { + plugins: [ + [ + '@vuepress/register-components', + { + components: { + FooBar: path.resolve(__dirname, './components/FooBar.vue'), + }, + }, + ], + ], +} +``` + +### componentsDir + +- Type: `string | null` + +- Default: `null` + +- Details: + + An absolute path of the components directory. + + Files in this directory which are matched with [componentsPatterns](#componentspatterns) will be registered as Vue components automatically. + +- Example: + +```js +module.exports = { + plugins: [ + [ + '@vuepress/register-components', + { + componentsDir: path.resolve(__dirname, './components'), + }, + ], + ], +} +``` + +Components directory: + +```bash +components +├─ FooBar.vue +└─ Baz.vue +``` + +Components will be registered like this: + +```js +import { defineAsyncComponent } from 'vue' + +app.component( + 'FooBar', + defineAsyncComponent(() => import('/path/to/components/FooBar.vue')) +) + +app.component( + 'Baz', + defineAsyncComponent(() => import('/path/to/components/Baz.vue')) +) +``` + +### componentsPatterns + +- Type: `string[]` + +- Default: `['**/*.vue']` + +- Details: + + Patterns to match component files using [globby](https://github.com/sindresorhus/globby). + + The patterns are relative to [componentsDir](#componentsdir). + +### getComponentName + +- Type: `(filename: string) => string` + +- Default: `(filename) => path.trimExt(filename.replace(/\/|\\/g, '-'))` + +- Details: + + A function to get component name from the filename. + + It will only take effect on the files in the [componentsDir](#componentsdir) which are matched with the [componentsPatterns](#componentspatterns). + + Notice that the `filename` is a filepath relative to [componentsDir](#componentsdir). diff --git a/docs/zh/guide/migration.md b/docs/zh/guide/migration.md index f895e769d1..3a6b58d63f 100644 --- a/docs/zh/guide/migration.md +++ b/docs/zh/guide/migration.md @@ -164,7 +164,7 @@ VuePress v1 的 Stylus 调色板系统 (即 `styles/palette.styl` 和 `styles/ 在该目录下的文件不会被自动注册为 Vue 组件。 -你需要在 `.vuepress/clientAppEnhance.{js,ts}` 中手动注册你的组件。 +你需要使用 `@vuepress/plugin-register-components` ,或者在 `.vuepress/clientAppEnhance.{js,ts}` 中手动注册你的组件。 #### .vuepress/theme/ @@ -268,7 +268,7 @@ v1 的主题和插件和 v2 并不兼容。 - 所谓的 **主题目录结构约定** 不再存在。 - `theme/enhanceApp.js` 或 `theme/clientAppEnhance.{js,ts}` 文件不会被隐式作为 Client App Enhance 文件。你需要在 `clientAppEnhanceFiles` Hook 中显式指定它。 - - `theme/global-components/` 目录下的文件不会被自动注册为 Vue 组件。你需要在 `clientAppEnhance.{js,ts}` 中手动注册组件。 + - `theme/global-components/` 目录下的文件不会被自动注册为 Vue 组件。你需要使用 `@vuepress/plugin-register-components` ,或者在 `clientAppEnhance.{js,ts}` 中手动注册组件。 - `theme/layouts/` 目录下的文件不会被自动注册为布局组件。你需要通过 `layouts` 配置项来显式指定。 - `theme/templates/` 目录下的文件不会被自动作为 dev / ssr 的模板。 - 你始终需要提供主题入口文件,并且不要使用 `"main": "layouts/Layout.vue"` 作为主题入口。 diff --git a/docs/zh/reference/plugin/register-components.md b/docs/zh/reference/plugin/register-components.md new file mode 100644 index 0000000000..38bc75c93a --- /dev/null +++ b/docs/zh/reference/plugin/register-components.md @@ -0,0 +1,115 @@ +# register-components + +> [@vuepress/plugin-register-components](https://www.npmjs.com/package/@vuepress/plugin-register-components) + +根据组件文件或目录自动注册 Vue 组件。 + +## 配置项 + +### components + +- 类型: `Record` + +- 默认值: `{}` + +- 详情: + + 一个定义了组件名称和其对应文件路径的对象。 + + 键会被用作组件名称,值是组件文件的绝对路径。 + + 如果该配置项中的组件名称和 [componentsDir](#componentsdir) 配置项发生冲突,那么该配置项会有更高的优先级。 + +- 示例: + +```js +module.exports = { + plugins: [ + [ + '@vuepress/register-components', + { + components: { + FooBar: path.resolve(__dirname, './components/FooBar.vue'), + }, + }, + ], + ], +} +``` + +### componentsDir + +- 类型: `string | null` + +- 默认值: `null` + +- 详情: + + 组件目录的绝对路径。 + + 该目录下匹配 [componentsPatterns](#componentspatterns) 的文件会被自动注册为 Vue 组件。 + +- 示例: + +```js +module.exports = { + plugins: [ + [ + '@vuepress/register-components', + { + componentsDir: path.resolve(__dirname, './components'), + }, + ], + ], +} +``` + +组件目录: + +```bash +components +├─ FooBar.vue +└─ Baz.vue +``` + +组件会像这样被注册: + +```js +import { defineAsyncComponent } from 'vue' + +app.component( + 'FooBar', + defineAsyncComponent(() => import('/path/to/components/FooBar.vue')) +) + +app.component( + 'Baz', + defineAsyncComponent(() => import('/path/to/components/Baz.vue')) +) +``` + +### componentsPatterns + +- 类型: `string[]` + +- 默认值: `['**/*.vue']` + +- 详情: + + 使用 [globby](https://github.com/sindresorhus/globby) 来匹配组件文件的 Patterns 。 + + 该 Patterns 是相对于 [componentsDir](#componentsdir) 目录的。 + +### getComponentName + +- 类型: `(filename: string) => string` + +- 默认值: `(filename) => path.trimExt(filename.replace(/\/|\\/g, '-'))` + +- 详情: + + 用于从文件名获取对应组件名称的函数。 + + 它只会对 [componentsDir](#componentsdir) 目录下匹配了 [componentsPatterns](#componentspatterns) 的文件生效。 + + 注意,这里的 `filename` 是相对于 [componentsPatterns](#componentspatterns) 目录的文件路径。 diff --git a/packages/@vuepress/plugin-register-components/package.json b/packages/@vuepress/plugin-register-components/package.json new file mode 100644 index 0000000000..7d9b4ff780 --- /dev/null +++ b/packages/@vuepress/plugin-register-components/package.json @@ -0,0 +1,39 @@ +{ + "name": "@vuepress/plugin-register-components", + "version": "2.0.0-beta.8", + "description": "VuePress plugin - register-components", + "keywords": [ + "vuepress-plugin", + "vuepress", + "plugin", + "component" + ], + "homepage": "https://github.com/vuepress", + "bugs": { + "url": "https://github.com/vuepress/vuepress-next/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vuepress/vuepress-next.git" + }, + "license": "MIT", + "author": "meteorlxy", + "main": "lib/node/index.js", + "types": "lib/node/index.d.ts", + "files": [ + "lib" + ], + "scripts": { + "build": "tsc -b tsconfig.build.json", + "clean": "rimraf lib *.tsbuildinfo", + "copy": "cpx \"src/**/*.{css,svg}\" lib" + }, + "dependencies": { + "@vuepress/core": "2.0.0-beta.8", + "@vuepress/utils": "2.0.0-beta.8", + "chokidar": "^3.5.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/@vuepress/plugin-register-components/src/node/getComponentsFromDir.ts b/packages/@vuepress/plugin-register-components/src/node/getComponentsFromDir.ts new file mode 100644 index 0000000000..062f68ca32 --- /dev/null +++ b/packages/@vuepress/plugin-register-components/src/node/getComponentsFromDir.ts @@ -0,0 +1,27 @@ +import { globby, path } from '@vuepress/utils' +import type { RegisterComponentsPluginOptions } from './registerComponentsPlugin' + +export const getComponentsFromDir = async ({ + componentsDir, + componentsPatterns, + getComponentName, +}: Omit): Promise< + Record +> => { + if (!componentsDir) { + return {} + } + + // get all matched component files + const componentsDirFiles = await globby(componentsPatterns, { + cwd: componentsDir, + }) + + // transform files to name => filepath map + return Object.fromEntries( + componentsDirFiles.map((filename) => [ + getComponentName(filename), + path.resolve(componentsDir, filename), + ]) + ) +} diff --git a/packages/@vuepress/plugin-register-components/src/node/index.ts b/packages/@vuepress/plugin-register-components/src/node/index.ts new file mode 100644 index 0000000000..bcb849a1f6 --- /dev/null +++ b/packages/@vuepress/plugin-register-components/src/node/index.ts @@ -0,0 +1,7 @@ +import { registerComponentsPlugin } from './registerComponentsPlugin' + +export * from './getComponentsFromDir' +export * from './prepareClientAppEnhanceFile' +export * from './registerComponentsPlugin' + +export default registerComponentsPlugin diff --git a/packages/@vuepress/plugin-register-components/src/node/prepareClientAppEnhanceFile.ts b/packages/@vuepress/plugin-register-components/src/node/prepareClientAppEnhanceFile.ts new file mode 100644 index 0000000000..0c8878a2fd --- /dev/null +++ b/packages/@vuepress/plugin-register-components/src/node/prepareClientAppEnhanceFile.ts @@ -0,0 +1,39 @@ +import type { App } from '@vuepress/core' +import { getComponentsFromDir } from './getComponentsFromDir' +import type { RegisterComponentsPluginOptions } from './registerComponentsPlugin' + +export const prepareClientAppEnhanceFile = async ( + app: App, + options: RegisterComponentsPluginOptions, + identifier: string +): Promise => { + // get components from directory + const componentsFromDir = await getComponentsFromDir(options) + + // components from options will override components from dir + // if they have the same component name + const componentsMap: Record = { + ...componentsFromDir, + ...options.components, + } + + // client app enhance file content + const content = `\ +import { defineAsyncComponent } from 'vue' + +export default ({ app }) => {\ +${Object.entries(componentsMap).map( + ([name, filepath]) => ` + app.component(${JSON.stringify( + name + )}, defineAsyncComponent(() => import(${JSON.stringify(filepath)})))` +)} +} +` + + // write temp file and return the file path + return app.writeTemp( + `register-components/clientAppEnhance.${identifier}.js`, + content + ) +} diff --git a/packages/@vuepress/plugin-register-components/src/node/registerComponentsPlugin.ts b/packages/@vuepress/plugin-register-components/src/node/registerComponentsPlugin.ts new file mode 100644 index 0000000000..d86f6ae4fa --- /dev/null +++ b/packages/@vuepress/plugin-register-components/src/node/registerComponentsPlugin.ts @@ -0,0 +1,58 @@ +import * as chokidar from 'chokidar' +import type { Plugin } from '@vuepress/core' +import { hash, path } from '@vuepress/utils' +import { prepareClientAppEnhanceFile } from './prepareClientAppEnhanceFile' + +export interface RegisterComponentsPluginOptions { + components: Record + componentsDir: string | null + componentsPatterns: string[] + getComponentName: (filename: string) => string +} + +export const registerComponentsPlugin: Plugin = ( + { + components = {}, + componentsDir = null, + componentsPatterns = ['**/*.vue'], + getComponentName = (filename) => + path.trimExt(filename.replace(/\/|\\/g, '-')), + }, + app +) => { + const options: RegisterComponentsPluginOptions = { + components, + componentsDir, + componentsPatterns, + getComponentName, + } + + // use options hash as the identifier of client app enhance file + // to avoid conflicts when using this plugin multiple times + const optionsHash = hash(options) + + return { + name: '@vuepress/plugin-register-components', + + multiple: true, + + clientAppEnhanceFiles: () => + prepareClientAppEnhanceFile(app, options, optionsHash), + + onWatched: (app, watchers) => { + if (componentsDir) { + const componentsWatcher = chokidar.watch(componentsPatterns, { + cwd: componentsDir, + ignoreInitial: true, + }) + componentsWatcher.on('add', () => { + prepareClientAppEnhanceFile(app, options, optionsHash) + }) + componentsWatcher.on('unlink', () => { + prepareClientAppEnhanceFile(app, options, optionsHash) + }) + watchers.push(componentsWatcher) + } + }, + } +} diff --git a/packages/@vuepress/plugin-register-components/tsconfig.build.json b/packages/@vuepress/plugin-register-components/tsconfig.build.json new file mode 100644 index 0000000000..b419ba3c9b --- /dev/null +++ b/packages/@vuepress/plugin-register-components/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["./src"], + "references": [ + { "path": "../core/tsconfig.build.json" }, + { "path": "../utils/tsconfig.build.json" } + ] +} diff --git a/packages/@vuepress/plugin-register-components/tsconfig.json b/packages/@vuepress/plugin-register-components/tsconfig.json new file mode 100644 index 0000000000..3114e03c34 --- /dev/null +++ b/packages/@vuepress/plugin-register-components/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["./src", "./__tests__"] +} diff --git a/tsconfig.json b/tsconfig.json index cc62661749..b348316154 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,6 +28,9 @@ }, { "path": "./packages/@vuepress/plugin-pwa/tsconfig.build.json" }, { "path": "./packages/@vuepress/plugin-pwa-popup/tsconfig.build.json" }, + { + "path": "./packages/@vuepress/plugin-register-components/tsconfig.build.json" + }, { "path": "./packages/@vuepress/plugin-shiki/tsconfig.build.json" }, { "path": "./packages/@vuepress/plugin-theme-data/tsconfig.build.json" }, { "path": "./packages/@vuepress/plugin-toc/tsconfig.build.json" },