diff --git a/packages/@vuepress/core/lib/internal-plugins/palette/index.js b/packages/@vuepress/core/lib/internal-plugins/palette/index.js index e61deacc49..3120e9e47b 100644 --- a/packages/@vuepress/core/lib/internal-plugins/palette/index.js +++ b/packages/@vuepress/core/lib/internal-plugins/palette/index.js @@ -26,12 +26,22 @@ module.exports = (options, ctx) => ({ const themePaletteContent = fs.existsSync(themePalette) ? `@import(${JSON.stringify(themePalette)})` : '' + const userPaletteContent = fs.existsSync(userPalette) ? `@import(${JSON.stringify(userPalette)})` : '' // user's palette can override theme's palette. - const paletteContent = themePaletteContent + userPaletteContent + let paletteContent = themePaletteContent + userPaletteContent + + if (ctx.parentThemePath) { + const parentThemePalette = path.resolve(ctx.parentThemePath, 'styles/palette.styl') + const parentThemePaletteContent = fs.existsSync(parentThemePalette) + ? `@import(${JSON.stringify(parentThemePalette)})` + : '' + paletteContent = parentThemePaletteContent + paletteContent + } + await writeTemp('palette.styl', paletteContent) } }) diff --git a/packages/@vuepress/core/lib/internal-plugins/style/index.js b/packages/@vuepress/core/lib/internal-plugins/style/index.js index 04fb10b773..115c1c1a8b 100644 --- a/packages/@vuepress/core/lib/internal-plugins/style/index.js +++ b/packages/@vuepress/core/lib/internal-plugins/style/index.js @@ -21,11 +21,21 @@ module.exports = (options, ctx) => ({ const themeStyleContent = fs.existsSync(themeStyle) ? `@import(${JSON.stringify(themeStyle)})` : '' + const userStyleContent = fs.existsSync(userStyle) ? `@import(${JSON.stringify(userStyle)})` : '' - const styleContent = themeStyleContent + userStyleContent + let styleContent = themeStyleContent + userStyleContent + + if (ctx.parentThemePath) { + const parentThemeStyle = path.resolve(ctx.parentThemePath, 'styles/index.styl') + const parentThemeStyleContent = fs.existsSync(parentThemeStyle) + ? `@import(${JSON.stringify(parentThemeStyle)})` + : '' + styleContent = parentThemeStyleContent + styleContent + } + await writeTemp('style.styl', styleContent) } }) diff --git a/packages/@vuepress/core/lib/prepare/AppContext.js b/packages/@vuepress/core/lib/prepare/AppContext.js index 1402037f2a..eda8085770 100644 --- a/packages/@vuepress/core/lib/prepare/AppContext.js +++ b/packages/@vuepress/core/lib/prepare/AppContext.js @@ -128,7 +128,8 @@ module.exports = class AppContext { .use('@vuepress/register-components', { componentsDir: [ path.resolve(this.sourceDir, '.vuepress/components'), - path.resolve(this.themePath, 'global-components') + path.resolve(this.themePath, 'global-components'), + this.parentThemePath && path.resolve(this.parentThemePath, 'global-components') ] }) } @@ -140,8 +141,11 @@ module.exports = class AppContext { */ applyUserPlugins () { + this.pluginAPI.useByPluginsConfig(this.cliOptions.plugins) + if (this.parentThemePath) { + this.pluginAPI.use(this.parentThemeEntryFile) + } this.pluginAPI - .useByPluginsConfig(this.cliOptions.plugins) .use(this.themeEntryFile) .use(Object.assign({}, this.siteConfig, { name: '@vuepress/internal-site-config' })) } @@ -193,6 +197,9 @@ module.exports = class AppContext { const themeSsrTemplate = path.resolve(this.themePath, 'templates/ssr.html') const themeDevTemplate = path.resolve(this.themePath, 'templates/dev.html') + const parentThemeSsrTemplate = path.resolve(this.themePath, 'templates/ssr.html') + const parentThemeDevTemplate = path.resolve(this.themePath, 'templates/dev.html') + const defaultSsrTemplate = path.resolve(__dirname, '../app/index.ssr.html') const defaultDevTemplate = path.resolve(__dirname, '../app/index.dev.html') @@ -200,6 +207,7 @@ module.exports = class AppContext { siteSsrTemplate, siteSsrTemplate2, themeSsrTemplate, + parentThemeSsrTemplate, defaultSsrTemplate ]) @@ -207,6 +215,7 @@ module.exports = class AppContext { siteDevTemplate, siteDevTemplate2, themeDevTemplate, + parentThemeDevTemplate, defaultDevTemplate ]) diff --git a/packages/@vuepress/core/lib/prepare/loadTheme.js b/packages/@vuepress/core/lib/prepare/loadTheme.js index 3b15c118b8..eccfeabe2d 100644 --- a/packages/@vuepress/core/lib/prepare/loadTheme.js +++ b/packages/@vuepress/core/lib/prepare/loadTheme.js @@ -43,21 +43,21 @@ module.exports = async function loadTheme (ctx) { let themeEntryFile = null // Optional let themeName let themeShortcut + let parentThemePath = null // Optional + let parentThemeEntryFile = null // Optional if (useLocalTheme) { themePath = localThemePath logger.tip(`\nApply theme located at ${chalk.gray(themePath)}...`) } else if (isString(theme)) { const resolved = themeResolver.resolve(theme, sourceDir) - const { entry: modulePath, name, shortcut } = resolved - if (modulePath === null) { + const { entry, name, shortcut } = resolved + + if (entry === null) { throw new Error(`Cannot resolve theme ${theme}.`) } - if (modulePath.endsWith('.js') || modulePath.endsWith('.vue')) { - themePath = path.parse(modulePath).dir - } else { - themePath = modulePath - } + + themePath = normalizeThemePath(resolved) themeName = name themeShortcut = shortcut logger.tip(`\nApply theme ${chalk.gray(themeName)}`) @@ -70,15 +70,43 @@ module.exports = async function loadTheme (ctx) { } catch (error) { themeEntryFile = {} } + themeEntryFile.name = '@vuepress/internal-theme-entry-file' themeEntryFile.shortcut = null // handle theme api const layoutDirs = [ - path.resolve(themePath, 'layouts'), - path.resolve(themePath, '.') + path.resolve(themePath, '.'), + path.resolve(themePath, 'layouts') ] + if (themeEntryFile.extend) { + const resolved = themeResolver.resolve(themeEntryFile.extend, sourceDir) + if (resolved.entry === null) { + throw new Error(`Cannot resolve parent theme ${themeEntryFile.extend}.`) + } + parentThemePath = normalizeThemePath(resolved) + + try { + parentThemeEntryFile = pluginAPI.normalizePlugin(parentThemePath, ctx.themeConfig) + } catch (error) { + parentThemeEntryFile = {} + } + + parentThemeEntryFile.name = '@vuepress/internal-parent-theme-entry-file' + parentThemeEntryFile.shortcut = null + + layoutDirs.unshift( + path.resolve(parentThemePath, '.'), + path.resolve(parentThemePath, 'layouts'), + ) + + themeEntryFile.alias = Object.assign( + themeEntryFile.alias || {}, + { '@parent-theme': parentThemePath } + ) + } + // normalize component name const getComponentName = filename => { filename = filename.slice(0, -4) @@ -139,6 +167,20 @@ module.exports = async function loadTheme (ctx) { layoutComponentMap, themeEntryFile, themeName, - themeShortcut + themeShortcut, + parentThemePath, + parentThemeEntryFile + } +} + +function normalizeThemePath (resolved) { + const { entry, name, fromDep } = resolved + if (fromDep) { + const pkgPath = require.resolve(`${name}/package.json`) + return path.parse(pkgPath).dir + } else if (entry.endsWith('.js') || entry.endsWith('.vue')) { + return path.parse(entry).dir + } else { + return entry } } diff --git a/packages/@vuepress/plugin-register-components/index.js b/packages/@vuepress/plugin-register-components/index.js index f3f548c643..d5927f3f85 100644 --- a/packages/@vuepress/plugin-register-components/index.js +++ b/packages/@vuepress/plugin-register-components/index.js @@ -1,4 +1,4 @@ -const { fs, path, globby } = require('@vuepress/shared-utils') +const { fs, path, globby, datatypes: { isString }} = require('@vuepress/shared-utils') function fileToComponentName (file) { return file @@ -40,6 +40,9 @@ module.exports = (options, context) => ({ // 1. Register components in specified directories for (const baseDir of baseDirs) { + if (!isString(baseDir)) { + continue + } const files = await resolveComponents(baseDir) || [] code += files.map(file => genImport(baseDir, file)).join('\n') + '\n' } diff --git a/packages/docs/docs/theme/option-api.md b/packages/docs/docs/theme/option-api.md index ad6350e075..ea8b753b54 100644 --- a/packages/docs/docs/theme/option-api.md +++ b/packages/docs/docs/theme/option-api.md @@ -9,4 +9,26 @@ seoTitle: Option API | Theme - Type: `Array|Object` - Default: undefined -See: [Plugin > Using a plugin](../plugin/using-a-plugin.md). +**Also see:** + +- [Plugin > Using a plugin](../plugin/using-a-plugin.md). + +## extend + +- Type: `String` +- Default: undefined + +```js +module.exports = { + extend: '@vuepress/theme-default' +} +``` + +VuePress supports a theme to be inherited from another theme. VuePress will follow the principle of `override` to automatically help you resolve the priorities of various theme attributes, such as styles, layout components. + +Note that in the child theme, VuePress will apply a `@parent-theme` [alias](../plugin/option-api.md#alias) pointing to the package directory of parent theme. + +**Also see:** + +- [Example: `@vuepress/theme-vue`](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/theme-vue) +- [Design Concepts of VuePress 1.x](../miscellaneous/design-concepts.md) diff --git a/packages/docs/docs/zh/theme/option-api.md b/packages/docs/docs/zh/theme/option-api.md index cd87e1cb1b..9a2fe2c819 100644 --- a/packages/docs/docs/zh/theme/option-api.md +++ b/packages/docs/docs/zh/theme/option-api.md @@ -9,4 +9,15 @@ seoTitle: Option API | Theme - 类型: `Array|Object` - 默认值: undefined -参考: [插件 > 使用插件](../plugin/using-a-plugin.md). +**参考:** + +- [插件 > 使用插件](../plugin/using-a-plugin.md). + +VuePress 支持一个主题继承于另一个主题。VuePress 将遵循 `override` 的方式自动帮你解决各种主题属性(如样式、布局组件)的优先级。 + +值得注意的是,在子主题中,VuePress 将注入一个指向父主题包目录根路径的 [alias](../plugin/option-api.md#alias) `@parent-theme`。 + +**参考:** + +- [例子: `@vuepress/theme-vue`](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/theme-vue) +- [Design Concepts of VuePress 1.x](../miscellaneous/design-concepts.md)