diff --git a/CHANGELOG.md b/CHANGELOG.md
old mode 100644
new mode 100755
index 25b98b18bf..44152f7090
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,51 +22,6 @@
### Features
* **$core:** support global layout (close: [#1226](https://github.com/vuejs/vuepress/issues/1226)) ([c91f55a](https://github.com/vuejs/vuepress/commit/c91f55a))
-
- From now on, users have the ability to use a custom global layout component via [siteConfig](https://v1.vuepress.vuejs.org/miscellaneous/glossary.html#siteconfig) or [themeEntryFile](https://v1.vuepress.vuejs.org/miscellaneous/glossary.html#themeentryfile):
-
- ```js
- module.exports = {
- globalLayout: '/path/to/your/global/vue/sfc'
- }
- ```
-
- Here is the [content of default global layout component](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/components/GlobalLayout.vue), an example of setting global header and footer:
-
- ```vue
-
-
-
-
-
-
-
-
-
- ```
-
- Also, you can follow the convention, directly create a component `.vuepress/components/GlobalLayout.vue` or `themePath/layouts/GlobalLayout.vue` without any config. the loading priority is as follows:
-
- - siteConfig
- - siteAgreement
- - themeEntryFile
- - themeAgreement
- - default
-
* **$theme-default:** disable search box via frontmatter (close: [#1287](https://github.com/vuejs/vuepress/issues/1287)) ([#1288](https://github.com/vuejs/vuepress/issues/1288)) ([54e9eb0](https://github.com/vuejs/vuepress/commit/54e9eb0))
diff --git a/__mocks__/vuepress-theme-child/Layout.vue b/__mocks__/vuepress-theme-child/Layout.vue
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__mocks__/vuepress-theme-child/components/Home.vue b/__mocks__/vuepress-theme-child/components/Home.vue
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__mocks__/vuepress-theme-child/index.js b/__mocks__/vuepress-theme-child/index.js
new file mode 100644
index 0000000000..8cf79b81cc
--- /dev/null
+++ b/__mocks__/vuepress-theme-child/index.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extend: 'vuepress-theme-parent'
+}
diff --git a/__mocks__/vuepress-theme-parent/Layout.vue b/__mocks__/vuepress-theme-parent/Layout.vue
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__mocks__/vuepress-theme-parent/components/Home.vue b/__mocks__/vuepress-theme-parent/components/Home.vue
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__mocks__/vuepress-theme-parent/components/Sidebar.vue b/__mocks__/vuepress-theme-parent/components/Sidebar.vue
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__mocks__/vuepress-theme-parent/index.js b/__mocks__/vuepress-theme-parent/index.js
new file mode 100644
index 0000000000..4ba52ba2c8
--- /dev/null
+++ b/__mocks__/vuepress-theme-parent/index.js
@@ -0,0 +1 @@
+module.exports = {}
diff --git a/packages/@vuepress/core/__tests__/theme-api/fixtures/theme/Layout.vue b/packages/@vuepress/core/__tests__/theme-api/fixtures/theme/Layout.vue
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/@vuepress/core/__tests__/theme-api/fixtures/theme/components/Home.vue b/packages/@vuepress/core/__tests__/theme-api/fixtures/theme/components/Home.vue
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/@vuepress/core/__tests__/theme-api/index.spec.js b/packages/@vuepress/core/__tests__/theme-api/index.spec.js
new file mode 100644
index 0000000000..a8e6c7bdd4
--- /dev/null
+++ b/packages/@vuepress/core/__tests__/theme-api/index.spec.js
@@ -0,0 +1,27 @@
+jest.mock('vuepress-theme-parent')
+jest.mock('vuepress-theme-child')
+
+import ThemeAPI from '../../lib/theme-api'
+import { resolve } from 'path'
+
+const theme = {
+ path: resolve(process.cwd(), '__mocks__/vuepress-theme-child'),
+ name: 'vuepress-theme-child',
+ shortcut: 'child',
+ entryFile: require('vuepress-theme-child')
+}
+
+const parent = {
+ path: resolve(process.cwd(), '__mocks__/vuepress-theme-parent'),
+ name: 'vuepress-theme-parent',
+ shortcut: 'parent',
+ entryFile: {}
+}
+
+describe('ThemeAPI', () => {
+ test('extend', async () => {
+ const themeAPI = new ThemeAPI(theme, parent)
+ console.log(themeAPI.theme.entry)
+ })
+ // loadTheme('vuepress-theme-child')
+})
diff --git a/packages/@vuepress/core/lib/internal-plugins/enhanceApp.js b/packages/@vuepress/core/lib/internal-plugins/enhanceApp.js
old mode 100644
new mode 100755
index 5aa987c38a..7e90d4d751
--- a/packages/@vuepress/core/lib/internal-plugins/enhanceApp.js
+++ b/packages/@vuepress/core/lib/internal-plugins/enhanceApp.js
@@ -4,12 +4,14 @@ module.exports = (options, context) => ({
name: '@vuepress/internal-enhance-app',
enhanceAppFiles () {
- const { sourceDir, themePath } = context
+ const { sourceDir, themeAPI } = context
const enhanceAppPath = path.resolve(sourceDir, '.vuepress/enhanceApp.js')
- const themeEnhanceAppPath = path.resolve(themePath, 'enhanceApp.js')
- return [
- enhanceAppPath,
- themeEnhanceAppPath
- ]
+ const files = [enhanceAppPath]
+ if (themeAPI.existsParentTheme) {
+ files.push(path.resolve(themeAPI.parentTheme.path, 'enhanceApp.js'))
+ }
+ const themeEnhanceAppPath = path.resolve(themeAPI.theme.path, 'enhanceApp.js')
+ files.push(themeEnhanceAppPath)
+ return files
}
})
diff --git a/packages/@vuepress/core/lib/internal-plugins/layoutComponents.js b/packages/@vuepress/core/lib/internal-plugins/layoutComponents.js
index 97f781b933..d8b71a2417 100644
--- a/packages/@vuepress/core/lib/internal-plugins/layoutComponents.js
+++ b/packages/@vuepress/core/lib/internal-plugins/layoutComponents.js
@@ -1,22 +1,13 @@
module.exports = (options, ctx) => {
- const { layoutComponentMap } = ctx
- const componentNames = Object.keys(layoutComponentMap)
-
return {
name: '@vuepress/internal-layout-components',
async clientDynamicModules () {
+ const componentNames = Object.keys(ctx.themeAPI.layoutComponentMap)
const code = `export default {\n${componentNames
- .map(name => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(layoutComponentMap[name].path)})`)
+ .map(name => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(ctx.themeAPI.layoutComponentMap[name].path)})`)
.join(',\n')} \n}`
return { name: 'layout-components.js', content: code, dirname: 'internal' }
- },
-
- chainWebpack (config, isServer) {
- const setAlias = (alias, raw) => config.resolve.alias.set(alias, raw)
- componentNames.forEach(name => {
- setAlias(`@${name}`, layoutComponentMap[name].path)
- })
}
}
}
diff --git a/packages/@vuepress/core/lib/internal-plugins/palette/index.js b/packages/@vuepress/core/lib/internal-plugins/palette/index.js
index fc0c2684b7..bd43d5e4fa 100644
--- a/packages/@vuepress/core/lib/internal-plugins/palette/index.js
+++ b/packages/@vuepress/core/lib/internal-plugins/palette/index.js
@@ -20,7 +20,7 @@ module.exports = (options, ctx) => ({
// 2. write palette.styl
const { sourceDir, writeTemp } = ctx
- const themePalette = path.resolve(ctx.themePath, 'styles/palette.styl')
+ const themePalette = path.resolve(ctx.themeAPI.theme.path, 'styles/palette.styl')
const userPalette = path.resolve(sourceDir, '.vuepress/styles/palette.styl')
const themePaletteContent = fs.existsSync(themePalette)
@@ -34,8 +34,8 @@ module.exports = (options, ctx) => ({
// user's palette can override theme's palette.
let paletteContent = themePaletteContent + userPaletteContent
- if (ctx.parentThemePath) {
- const parentThemePalette = path.resolve(ctx.parentThemePath, 'styles/palette.styl')
+ if (ctx.themeAPI.existsParentTheme) {
+ const parentThemePalette = path.resolve(ctx.themeAPI.parentTheme.path, 'styles/palette.styl')
const parentThemePaletteContent = fs.existsSync(parentThemePalette)
? `@import(${JSON.stringify(parentThemePalette.replace(/[\\]+/g, '/'))})`
: ''
diff --git a/packages/@vuepress/core/lib/internal-plugins/style/index.js b/packages/@vuepress/core/lib/internal-plugins/style/index.js
index a0646d7d9c..2055be6978 100644
--- a/packages/@vuepress/core/lib/internal-plugins/style/index.js
+++ b/packages/@vuepress/core/lib/internal-plugins/style/index.js
@@ -1,12 +1,16 @@
const { fs, path, logger, chalk } = require('@vuepress/shared-utils')
+/**
+ * @param options
+ * @param {AppContext} ctx
+ */
module.exports = (options, ctx) => ({
name: '@vuepress/internal-style',
enhanceAppFiles: [path.resolve(__dirname, 'client.js')],
async ready () {
- const { sourceDir, writeTemp } = ctx
+ const { sourceDir, writeTemp, themeAPI } = ctx
const overridePath = path.resolve(sourceDir, '.vuepress/override.styl')
const hasUserOverride = fs.existsSync(overridePath)
@@ -15,7 +19,7 @@ module.exports = (options, ctx) => ({
logger.tip(`${chalk.magenta('override.styl')} has been deprecated from v1.0.0, using ${chalk.cyan('.vuepress/styles/palette.styl')} instead.\n`)
}
- const themeStyle = path.resolve(ctx.themePath, 'styles/index.styl')
+ const themeStyle = path.resolve(themeAPI.theme.path, 'styles/index.styl')
const userStyle = path.resolve(sourceDir, '.vuepress/styles/index.styl')
const themeStyleContent = fs.existsSync(themeStyle)
@@ -28,8 +32,8 @@ module.exports = (options, ctx) => ({
let styleContent = themeStyleContent + userStyleContent
- if (ctx.parentThemePath) {
- const parentThemeStyle = path.resolve(ctx.parentThemePath, 'styles/index.styl')
+ if (themeAPI.existsParentTheme) {
+ const parentThemeStyle = path.resolve(themeAPI.parentTheme.path, 'styles/index.styl')
const parentThemeStyleContent = fs.existsSync(parentThemeStyle)
? `@import(${JSON.stringify(parentThemeStyle.replace(/[\\]+/g, '/'))})`
: ''
diff --git a/packages/@vuepress/core/lib/plugin-api/index.js b/packages/@vuepress/core/lib/plugin-api/index.js
index 83639fb542..a887fe7dd9 100644
--- a/packages/@vuepress/core/lib/plugin-api/index.js
+++ b/packages/@vuepress/core/lib/plugin-api/index.js
@@ -84,7 +84,11 @@ module.exports = class PluginAPI {
if (isPlainObject(pluginRaw) && pluginRaw.$$normalized) {
plugin = pluginRaw
} else {
- plugin = this.normalizePlugin(pluginRaw, pluginOptions)
+ try {
+ plugin = this.normalizePlugin(pluginRaw, pluginOptions)
+ } catch (e) {
+ logger.warn(e.message)
+ }
}
if (plugin.multiple !== true) {
@@ -114,8 +118,7 @@ module.exports = class PluginAPI {
normalizePlugin (pluginRaw, pluginOptions = {}) {
let plugin = this._pluginResolver.resolve(pluginRaw)
if (!plugin.entry) {
- console.warn(`[vuepress] cannot resolve plugin "${pluginRaw}"`)
- return this
+ throw new Error(`[vuepress] cannot resolve plugin "${pluginRaw}"`)
}
plugin = flattenPlugin(plugin, pluginOptions, this._pluginContext, this)
plugin.$$normalized = true
diff --git a/packages/@vuepress/core/lib/prepare/AppContext.js b/packages/@vuepress/core/lib/prepare/AppContext.js
old mode 100644
new mode 100755
index e92f2bc312..f769842f87
--- a/packages/@vuepress/core/lib/prepare/AppContext.js
+++ b/packages/@vuepress/core/lib/prepare/AppContext.js
@@ -94,7 +94,7 @@ module.exports = class AppContext {
this.resolveConfigAndInitialize()
this.resolveCacheLoaderOptions()
this.normalizeHeadTagUrls()
- await this.resolveTheme()
+ this.themeAPI = loadTheme(this)
this.resolveTemplates()
this.resolveGlobalLayout()
@@ -137,7 +137,7 @@ module.exports = class AppContext {
)
this.pluginAPI
- // internl core plugins
+ // internl core plugins
.use(require('../internal-plugins/siteData'))
.use(require('../internal-plugins/routes'))
.use(require('../internal-plugins/rootMixins'))
@@ -153,8 +153,8 @@ module.exports = class AppContext {
.use('@vuepress/register-components', {
componentsDir: [
path.resolve(this.sourceDir, '.vuepress/components'),
- path.resolve(this.themePath, 'global-components'),
- this.parentThemePath && path.resolve(this.parentThemePath, 'global-components')
+ path.resolve(this.themeAPI.theme.path, 'global-components'),
+ this.themeAPI.existsParentTheme && path.resolve(this.themeAPI.parentTheme.path, 'global-components')
]
})
}
@@ -167,11 +167,12 @@ module.exports = class AppContext {
applyUserPlugins () {
this.pluginAPI.useByPluginsConfig(this.cliOptions.plugins)
- if (this.parentThemePath) {
- this.pluginAPI.use(this.parentThemeEntryFile)
+ if (this.themeAPI.existsParentTheme) {
+ this.pluginAPI.use(this.themeAPI.parentTheme.entry)
}
this.pluginAPI
- .use(this.themeEntryFile)
+ .use(this.themeAPI.theme.entry)
+ .use(this.themeAPI.vuepressPlugin)
.use(Object.assign({}, this.siteConfig, { name: '@vuepress/internal-site-config' }))
}
@@ -221,41 +222,26 @@ module.exports = class AppContext {
*/
resolveTemplates () {
- const { siteSsrTemplate, siteDevTemplate } = this.siteConfig
-
- const templateDir = path.resolve(this.vuepressDir, 'templates')
- const siteSsrTemplate2 = path.resolve(templateDir, 'ssr.html')
- const siteDevTemplate2 = path.resolve(templateDir, 'dev.html')
-
- 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')
-
- const ssrTemplate = fsExistsFallback([
- siteSsrTemplate,
- siteSsrTemplate2,
- themeSsrTemplate,
- parentThemeSsrTemplate,
- defaultSsrTemplate
- ])
+ this.devTemplate = this.resolveCommonAgreementFilePath(
+ 'devTemplate',
+ {
+ defaultValue: path.resolve(__dirname, '../app/index.dev.html'),
+ siteAgreement: 'templates/dev.html',
+ themeAgreement: 'templates/dev.html'
+ }
+ )
- const devTemplate = fsExistsFallback([
- siteDevTemplate,
- siteDevTemplate2,
- themeDevTemplate,
- parentThemeDevTemplate,
- defaultDevTemplate
- ])
+ this.ssrTemplate = this.resolveCommonAgreementFilePath(
+ 'ssrTemplate',
+ {
+ defaultValue: path.resolve(__dirname, '../app/index.ssr.html'),
+ siteAgreement: 'templates/ssr.html',
+ themeAgreement: 'templates/ssr.html'
+ }
+ )
- logger.debug('SSR Template File: ' + chalk.gray(ssrTemplate))
- logger.debug('DEV Template File: ' + chalk.gray(devTemplate))
- this.devTemplate = devTemplate
- this.ssrTemplate = ssrTemplate
+ logger.debug('SSR Template File: ' + chalk.gray(this.ssrTemplate))
+ logger.debug('DEV Template File: ' + chalk.gray(this.devTemplate))
}
/**
@@ -266,14 +252,12 @@ module.exports = class AppContext {
*/
resolveGlobalLayout () {
- const GLOBAL_LAYOUT_COMPONENT_NAME = `GlobalLayout`
-
this.globalLayout = this.resolveCommonAgreementFilePath(
'globalLayout',
{
- defaultValue: path.resolve(__dirname, `../app/components/${GLOBAL_LAYOUT_COMPONENT_NAME}.vue`),
- siteAgreement: `components/${GLOBAL_LAYOUT_COMPONENT_NAME}.vue`,
- themeAgreement: `layouts/${GLOBAL_LAYOUT_COMPONENT_NAME}.vue`
+ defaultValue: path.resolve(__dirname, `../app/components/GlobalLayout.vue`),
+ siteAgreement: `components/GlobalLayout.vue`,
+ themeAgreement: `layouts/GlobalLayout.vue`
}
)
@@ -354,17 +338,6 @@ module.exports = class AppContext {
this.pages.push(page)
}
- /**
- * Resolve theme
- *
- * @returns {Promise}
- * @api private
- */
-
- async resolveTheme () {
- Object.assign(this, (await loadTheme(this)))
- }
-
/**
* Get config value of current active theme.
*
@@ -374,7 +347,8 @@ module.exports = class AppContext {
*/
getThemeConfigValue (key) {
- return this.themeEntryFile[key] || this.parentThemeEntryFile[key]
+ return this.themeAPI.theme.entry[key]
+ || this.themeAPI.existsParentTheme && this.themeAPI.parentTheme.entry[key]
}
/**
@@ -386,12 +360,12 @@ module.exports = class AppContext {
*/
resolveThemeAgreementFile (filepath) {
- const current = path.resolve(this.themePath, filepath)
+ const current = path.resolve(this.themeAPI.theme.path, filepath)
if (fs.existsSync(current)) {
return current
}
- if (this.parentThemePath) {
- const parent = path.resolve(this.parentThemePath, filepath)
+ if (this.themeAPI.existsParentTheme) {
+ const parent = path.resolve(this.themeAPI.theme.path, filepath)
if (fs.existsSync(parent)) {
return parent
}
diff --git a/packages/@vuepress/core/lib/prepare/loadTheme.js b/packages/@vuepress/core/lib/prepare/loadTheme.js
old mode 100644
new mode 100755
index f07804cddb..9a4bbca3d9
--- a/packages/@vuepress/core/lib/prepare/loadTheme.js
+++ b/packages/@vuepress/core/lib/prepare/loadTheme.js
@@ -5,11 +5,13 @@
*/
const {
- fs, path,
+ fs,
+ path: { resolve, parse },
moduleResolver: { getThemeResolver },
datatypes: { isString },
logger, chalk
} = require('@vuepress/shared-utils')
+const ThemeAPI = require('../theme-api')
/**
* Resolve theme.
@@ -25,164 +27,86 @@ const {
* @param {string} theme
* @param {string} sourceDir
* @param {string} vuepressDir
- * @returns {Promise}
+ * @returns {ThemeAPI}
*/
-module.exports = async function loadTheme (ctx) {
- const { siteConfig, cliOptions, sourceDir, vuepressDir, pluginAPI } = ctx
- const theme = siteConfig.theme || cliOptions.theme
+module.exports = function loadTheme (ctx) {
const themeResolver = getThemeResolver()
- const localThemePath = path.resolve(vuepressDir, 'theme')
- const useLocalTheme
- = !fs.existsSync(theme)
- && fs.existsSync(localThemePath)
- && (fs.readdirSync(localThemePath)).length > 0
-
- let themePath = null // Mandatory
- let themeEntryFile = {} // Optional
- let themeName
- let themeShortcut
- let parentThemePath = null // Optional
- let parentThemeEntryFile = {} // Optional
-
- if (useLocalTheme) {
- themePath = localThemePath
- logger.tip(`Apply theme located at ${chalk.gray(themePath)}...`)
- } else if (isString(theme)) {
- const resolved = themeResolver.resolve(theme, sourceDir)
- const { entry, name, shortcut } = resolved
-
- if (entry === null) {
- throw new Error(`Cannot resolve theme ${theme}.`)
- }
-
- themePath = normalizeThemePath(resolved)
- themeName = name
- themeShortcut = shortcut
- logger.tip(`Apply theme ${chalk.gray(themeName)}`)
- } else {
+ const theme = resolveTheme(ctx, themeResolver)
+ if (!theme.path) {
throw new Error(`[vuepress] You must specify a theme, or create a local custom theme. \n For more details, refer to https://vuepress.vuejs.org/guide/custom-themes.html#custom-themes. \n`)
}
+ logger.tip(`Apply theme ${chalk.gray(theme.name)}`)
+ theme.entry.name = '@vuepress/internal-theme-entry-file'
- try {
- themeEntryFile = pluginAPI.normalizePlugin(themePath, ctx.themeConfig)
- } catch (error) {
- themeEntryFile = {}
+ let parentTheme = {}
+ if (theme.entry.extend) {
+ parentTheme = resolveTheme(ctx, themeResolver, true, theme.entry.extend)
+ parentTheme.entry.name = '@vuepress/internal-parent-theme-entry-file'
}
- themeEntryFile.name = '@vuepress/internal-theme-entry-file'
- themeEntryFile.shortcut = null
-
- // handle theme api
- const layoutDirs = [
- 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 }
- )
- }
+ logger.debug('theme', theme.name, theme.path)
+ logger.debug('parentTheme', parentTheme.name, parentTheme.path)
+ return new ThemeAPI(theme, parentTheme, ctx)
+}
- // normalize component name
- const getComponentName = filename => {
- filename = filename.slice(0, -4)
- if (filename === '404') {
- filename = 'NotFound'
+function normalizeThemePath (resolved) {
+ const { entry, name, fromDep } = resolved
+ if (fromDep) {
+ const pkgPath = require.resolve(name)
+ let packageRootDir = parse(pkgPath).dir
+ // For those cases that "main" field was set to non-index file
+ // e.g. `layouts/Layout.vue`
+ while (!fs.existsSync(`${packageRootDir}/package.json`)) {
+ packageRootDir = resolve(packageRootDir, '..')
}
- return filename
+ return packageRootDir
+ } else if (entry.endsWith('.js') || entry.endsWith('.vue')) {
+ return parse(entry).dir
+ } else {
+ return entry
}
+}
- const readdirSync = dir => fs.existsSync(dir) && fs.readdirSync(dir) || []
-
- // built-in named layout or not.
- const isInternal = componentName => componentName === 'Layout'
- || componentName === 'NotFound'
-
- const layoutComponentMap = layoutDirs
- .map(
- layoutDir => readdirSync(layoutDir)
- .filter(filename => filename.endsWith('.vue'))
- .map(filename => {
- const componentName = getComponentName(filename)
- return {
- filename,
- componentName,
- isInternal: isInternal(componentName),
- path: path.resolve(layoutDir, filename)
- }
- })
- )
-
- .reduce((arr, next) => {
- arr.push(...next)
- return arr
- }, [])
-
- .reduce((map, component) => {
- map[component.componentName] = component
- return map
- }, {})
+function resolveTheme (ctx, resolver, ignoreLocal, theme) {
+ const { siteConfig, cliOptions, sourceDir, vuepressDir, pluginAPI } = ctx
+ const localThemePath = resolve(vuepressDir, 'theme')
+ theme = theme || siteConfig.theme || cliOptions.theme
- const { Layout = {}, NotFound = {}} = layoutComponentMap
+ let path
+ let name
+ let shortcut
+ let entry = {}
- // layout component does not exist.
- if (!Layout || !fs.existsSync(Layout.path)) {
- throw new Error(`[vuepress] Cannot resolve Layout.vue file in \n ${Layout.path}`)
- }
+ // 1. From local
+ if (!ignoreLocal
+ && !fs.existsSync(theme)
+ && fs.existsSync(localThemePath)
+ && (fs.readdirSync(localThemePath)).length > 0
+ ) {
+ path = localThemePath
+ name = shortcut = 'local'
+ logger.tip(`Apply local theme at ${chalk.gray(path)}...`)
- // use default 404 component.
- if (!NotFound || !fs.existsSync(NotFound.path)) {
- layoutComponentMap.NotFound = {
- filename: 'NotFound.vue',
- componentName: 'NotFound',
- path: path.resolve(__dirname, '../app/components/NotFound.vue'),
- isInternal: true
+ // 2. From dep
+ } else if (isString(theme)) {
+ const resolved = resolver.resolve(theme, sourceDir)
+ if (resolved.entry === null) {
+ throw new Error(`Cannot resolve theme: ${theme}.`)
}
+ path = normalizeThemePath(resolved)
+ name = resolved.name
+ shortcut = resolved.shortcut
+ } else {
+ return {}
}
- return {
- themePath,
- layoutComponentMap,
- themeEntryFile,
- themeName,
- themeShortcut,
- parentThemePath,
- parentThemeEntryFile
+ try {
+ entry = pluginAPI.normalizePlugin(path, ctx.themeConfig)
+ } catch (error) {
+ entry = {}
}
-}
-function normalizeThemePath (resolved) {
- const { entry, name, fromDep } = resolved
- if (fromDep) {
- const pkgPath = require.resolve(name)
- return path.parse(pkgPath).dir
- } else if (entry.endsWith('.js') || entry.endsWith('.vue')) {
- return path.parse(entry).dir
- } else {
- return entry
- }
+ return { path, name, shortcut, entry }
}
diff --git a/packages/@vuepress/core/lib/theme-api/Layout.fallback.vue b/packages/@vuepress/core/lib/theme-api/Layout.fallback.vue
new file mode 100644
index 0000000000..34d84c1b5f
--- /dev/null
+++ b/packages/@vuepress/core/lib/theme-api/Layout.fallback.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packages/@vuepress/core/lib/theme-api/index.js b/packages/@vuepress/core/lib/theme-api/index.js
new file mode 100644
index 0000000000..28af84c4ab
--- /dev/null
+++ b/packages/@vuepress/core/lib/theme-api/index.js
@@ -0,0 +1,152 @@
+const { logger, fs, path: { resolve }} = require('@vuepress/shared-utils')
+const readdirSync = dir => fs.existsSync(dir) && fs.readdirSync(dir) || []
+
+module.exports = class ThemeAPI {
+ constructor (theme, parentTheme) {
+ this.theme = theme
+ this.parentTheme = parentTheme || {}
+ this.existsParentTheme = !!this.parentTheme.path
+ this.vuepressPlugin = {
+ name: '@vuepress/internal-theme-api',
+ alias: {}
+ }
+ this.init()
+ }
+
+ setAlias (alias) {
+ this.vuepressPlugin.alias = {
+ ...this.vuepressPlugin.alias,
+ ...alias
+ }
+ }
+
+ init () {
+ const alias = {
+ '@current-theme': this.theme.path
+ }
+ if (this.existsParentTheme) {
+ alias['@parent-theme'] = this.parentTheme.path
+ }
+ this.componentMap = this.getComponents()
+ this.layoutComponentMap = this.getLayoutComponentMap()
+
+ Object.keys(this.componentMap).forEach((name) => {
+ const { filename, path } = this.componentMap[name]
+ alias[`@theme/components/${filename}`] = path
+ })
+
+ Object.keys(this.layoutComponentMap).forEach((name) => {
+ const { filename, path } = this.layoutComponentMap[name]
+ alias[`@theme/layouts/${filename}`] = path
+ })
+ alias['@theme'] = this.theme.path
+ this.setAlias(alias)
+ }
+
+ getComponents () {
+ const componentDirs = [
+ resolve(this.theme.path, 'components')
+ ]
+ if (this.existsParentTheme) {
+ componentDirs.unshift(
+ resolve(this.parentTheme.path, 'components'),
+ )
+ }
+ return resolveSFCs(componentDirs)
+ }
+
+ getLayoutComponentMap () {
+ const layoutDirs = [
+ resolve(this.theme.path, '.'),
+ resolve(this.theme.path, 'layouts')
+ ]
+ if (this.existsParentTheme) {
+ layoutDirs.unshift(
+ resolve(this.parentTheme.path, '.'),
+ resolve(this.parentTheme.path, 'layouts'),
+ )
+ }
+ // built-in named layout or not.
+ const layoutComponentMap = resolveSFCs(layoutDirs)
+
+ const { Layout = {}, NotFound = {}} = layoutComponentMap
+ // layout component does not exist.
+ if (!Layout || !fs.existsSync(Layout.path)) {
+ const fallbackLayoutPath = resolve(__dirname, 'Layout.fallback.vue')
+ layoutComponentMap.Layout = {
+ filename: 'Layout.vue',
+ componentName: 'Layout',
+ path: fallbackLayoutPath,
+ isInternal: true
+ }
+ logger.warn(
+ `[vuepress] Cannot resolve Layout.vue file in \n ${Layout.path},`
+ + `fallback to default layout: ${fallbackLayoutPath}`
+ )
+ }
+ if (!NotFound || !fs.existsSync(NotFound.path)) {
+ layoutComponentMap.NotFound = {
+ filename: 'NotFound.vue',
+ componentName: 'NotFound',
+ path: resolve(__dirname, '../app/components/NotFound.vue'),
+ isInternal: true
+ }
+ }
+ return layoutComponentMap
+ }
+}
+
+/**
+ * Resolve Vue SFCs, return a Map
+ *
+ * @param dirs
+ * @returns {*}
+ */
+
+function resolveSFCs (dirs) {
+ return dirs.map(
+ layoutDir => readdirSync(layoutDir)
+ .filter(filename => filename.endsWith('.vue'))
+ .map(filename => {
+ const componentName = getComponentName(filename)
+ return {
+ filename,
+ componentName,
+ isInternal: isInternal(componentName),
+ path: resolve(layoutDir, filename)
+ }
+ })
+ ).reduce((arr, next) => {
+ arr.push(...next)
+ return arr
+ }, []).reduce((map, component) => {
+ map[component.componentName] = component
+ return map
+ }, {})
+}
+
+/**
+ * normalize component name
+ *
+ * @param {strin} filename
+ * @returns {string}
+ */
+
+function getComponentName (filename) {
+ filename = filename.slice(0, -4)
+ if (filename === '404') {
+ filename = 'NotFound'
+ }
+ return filename
+}
+
+/**
+ * Whether it's agreed layout component
+ *
+ * @param name
+ * @returns {boolean}
+ */
+
+function isInternal (name) {
+ return name === 'Layout' || name === 'NotFound'
+}
diff --git a/packages/@vuepress/core/lib/webpack/createBaseConfig.js b/packages/@vuepress/core/lib/webpack/createBaseConfig.js
index c6b5a90cc1..bf79b2072f 100644
--- a/packages/@vuepress/core/lib/webpack/createBaseConfig.js
+++ b/packages/@vuepress/core/lib/webpack/createBaseConfig.js
@@ -15,7 +15,6 @@ module.exports = function createBaseConfig ({
sourceDir,
outDir,
base: publicPath,
- themePath,
markdown,
tempPath,
cacheDirectory,
@@ -52,7 +51,6 @@ module.exports = function createBaseConfig ({
config.resolve
.set('symlinks', true)
.alias
- .set('@theme', themePath)
.set('@source', sourceDir)
.set('@app', path.resolve(__dirname, '../app'))
.set('@temp', tempPath)
diff --git a/packages/@vuepress/theme-blog/layouts/Layout.vue b/packages/@vuepress/theme-blog/layouts/Layout2.vue
old mode 100644
new mode 100755
similarity index 100%
rename from packages/@vuepress/theme-blog/layouts/Layout.vue
rename to packages/@vuepress/theme-blog/layouts/Layout2.vue
diff --git a/packages/@vuepress/theme-default/components/DropdownLink.vue b/packages/@vuepress/theme-default/components/DropdownLink.vue
index ec45fae957..0d360830ee 100644
--- a/packages/@vuepress/theme-default/components/DropdownLink.vue
+++ b/packages/@vuepress/theme-default/components/DropdownLink.vue
@@ -50,8 +50,8 @@
+```
+
+On this premise, when you create a `Navbar` component in the same place in the child theme
+
+::: vue
+theme
+└── components
+ └── `Navbar.vue`
+:::
+
+`@theme/components/Navbar.vue` will automatically map to the Navbar component in the child theme. and when you remove the component, `@theme/components/Navbar.vue` will automatically restore to the Navbar component in the parent theme.
+
+In this way, you can easily "tamper" with some part of an atomic theme.
+
+::: tip
+1. You'd better override the component based on the code of the corresponding component in the parent theme.
+2. Currently, when developing theme locally, you need to manually restart dev server when a component is created or removed.
+:::
+
+## Access Parent Theme
+
+You can use `@parent-theme` to access the root path of the parent theme. The following example shows creating a layout component with the same name in a child theme and simply using slots in the parent theme. [@vuepress/theme-vue](https://github.com/vuejs/vuepress/tree/master/packages/%40vuepress/theme-vue) is created in this way.
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+
+
diff --git a/packages/docs/docs/theme/option-api.md b/packages/docs/docs/theme/option-api.md
old mode 100644
new mode 100755
index 0fbb7aa90c..57ac02cd9c
--- a/packages/docs/docs/theme/option-api.md
+++ b/packages/docs/docs/theme/option-api.md
@@ -1,8 +1,33 @@
---
-metaTitle: Option API | Theme
+metaTitle: Configuration | Theme
---
-# Option API
+# Theme Configuration
+
+As with plugins, the theme configuration file `themeEntry` should export a `plain JavaScript object`(`#1`). If the plugin needs to take options, it can be a function that exports a plain object(`#2`). The function will be called with the `siteConfig.themeConfig` as the first argument, along with [ctx](./context-api.md) which provides some compile-time metadata.
+
+``` js
+// #1
+module.exports = {
+ // ...
+}
+```
+
+``` js
+// #2
+module.exports = (themeConfig, ctx) => {
+ return {
+ // ...
+ }
+}
+```
+
+
+::: tip
+1. You should see the difference between `themeEntry` and `themeConfig`, the former is a configuration for ths theme itself, which is provided by VuePress. the latter is the user's configuration for the theme, which is implemented by the currently used theme, e.g. [Default Theme Config](./default-theme-config.md).
+
+2. In addition to the options listed in this section, `themeEntry` also supports all [Option API](../plugin/option-api.md) and [Life Cycle](../plugin/life-cycle.md) supported by plugins.
+:::
## plugins
@@ -11,9 +36,33 @@ metaTitle: Option API | Theme
**Also see:**
-- [Plugin > Using a plugin](../plugin/using-a-plugin.md).
+- [Plugin > Using a Plugin](../plugin/using-a-plugin.md).
+
+---
+
+::: warning
+You probably don't need to use following options tagged with unless you know what you are doing!
+:::
+
+## devTemplate
+
+- Type: `String`
+- Default: undefined
+
+HTML template path used in `dev` mode, default template see [here](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/index.dev.html)
+
+## ssrTemplate
+
+- Type: `String`
+- Default: undefined
+
+HTML template path used in `build` mode, default template see [here](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/index.ssr.html)
+
+**Also see:**
+
+- [Vue SSR Guide > template](https://ssr.vuejs.org/api/#template).
-## extend
+## extend
- Type: `String`
- Default: undefined
@@ -24,11 +73,52 @@ module.exports = {
}
```
-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.
+VuePress provides the ability to inherit one theme from another. VuePress will follow the concept of `override` and automatically help you prioritize various thematic attributes, e.g. styles and layout components.
**Also see:**
-- [Example: `@vuepress/theme-vue`](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/theme-vue)
+- [Theme Inheritance](./inheritance.md)
- [Design Concepts of VuePress 1.x](../miscellaneous/design-concepts.md)
+
+## globalLayout
+
+- Type: `String`
+- Default: undefined
+
+```js
+// themePath/index.js
+module.exports = {
+ globalLayout: '/path/to/your/global/vue/sfc'
+}
+```
+
+Global layout component is a component responsible for the global layout strategy. The [default global layout](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/components/GlobalLayout.vue) will help you render different layouts according to [$frontmatter.layout](../guide/frontmatter.md#layout), so in most cases you do not need to configure this option.
+
+For example, when you want to set a global header and footer for your theme, you can do this:
+
+```vue
+
+
+
+
+
+
+
+
+
+
+```
diff --git a/packages/docs/docs/theme/using-a-theme.md b/packages/docs/docs/theme/using-a-theme.md
index 1eac4ceff8..f441a9fc5c 100644
--- a/packages/docs/docs/theme/using-a-theme.md
+++ b/packages/docs/docs/theme/using-a-theme.md
@@ -14,7 +14,7 @@ module.exports = {
## Theme Shorthand
-If you prefix the plugin with `vuepress-theme-`, you can use a shorthand to leave out that prefix:
+If you prefix the theme with `vuepress-theme-`, you can use a shorthand to leave out that prefix:
``` js
module.exports = {
@@ -47,5 +47,5 @@ module.exports = {
```
::: warning Note
-The plugin whose name starts with `@vuepress/theme-` is an officially maintained theme.
+The theme whose name starts with `@vuepress/theme-` is an officially maintained theme.
:::
diff --git a/packages/docs/docs/theme/writing-a-theme.md b/packages/docs/docs/theme/writing-a-theme.md
index cce052cc3f..9ddad11902 100644
--- a/packages/docs/docs/theme/writing-a-theme.md
+++ b/packages/docs/docs/theme/writing-a-theme.md
@@ -34,22 +34,22 @@ Just one `Layout.vue` might not be enough, and you might also want to define mor
So it's time to reorganize your theme, an agreed theme directory structure is as follows:
::: vue
-themePath
-├── `global-components` _(**Optional**)_
+theme
+├── `global-components`
│ └── xxx.vue
-├── `components` _(**Optional**)_
+├── `components`
│ └── xxx.vue
├── `layouts`
-│ ├── Layout.vue _(**Required**)_
-│ └── 404.vue _(**Optional**)_
-├── `styles` _(**Optional**)_
+│ ├── Layout.vue _(**必要的**)_
+│ └── 404.vue
+├── `styles`
│ ├── index.styl
│ └── palette.styl
-├── `templates` _(**Optional**)_
+├── `templates`
│ ├── dev.html
│ └── ssr.html
-├── `index.js` _(**Only required when you publish it as an npm package**)_
-├── `enhanceApp.js` _(**Optional**)_
+├── `index.js`
+├── `enhanceApp.js`
└── package.json
:::
@@ -62,12 +62,11 @@ themePath
- `theme/enhanceApp.js`: Theme level enhancements.
::: warning Note
-When you want to publish your theme as an npm package, make sure the package has `index.js`, and set `"main"` field at `package.json` to `index.js` so that VuePress can resolve and get the correct [themePath](../miscellaneous/glossary.md#theme-side).
-
+When you publish your theme as an NPM package, if you don't have any theme configuration, that means you don't have `theme/index.js`, you'll need to set the `"main"` field to `layouts/Layout.vue` in `package.json`, only in this way VuePress can correctly resolve the theme.
```json
{
...
- "main": "index.js"
+ "main": "layouts/Layout.vue",
...
}
```
@@ -96,6 +95,10 @@ layout: AnotherLayout
---
````
+::: tip
+Each layout component may render distinct pages. If you want to apply some global UI (e.g. global header), consider using [globalLayout](./option-api.md#globallayout)。
+:::
+
## Apply plugins
You can apply some plugins to the theme via `theme/index.js`.
diff --git a/packages/docs/docs/zh/miscellaneous/glossary.md b/packages/docs/docs/zh/miscellaneous/glossary.md
old mode 100644
new mode 100755
index 95419a4aa2..fa60adaf73
--- a/packages/docs/docs/zh/miscellaneous/glossary.md
+++ b/packages/docs/docs/zh/miscellaneous/glossary.md
@@ -6,64 +6,75 @@ sidebar: auto
你可能会在文档中碰到一些陌生的概念,本节列出了文档中常见的术语,方便查阅、学习、插件/主题开发之用。
-## frontmatter
+## layout
-> Access: `$page.frontmatter`
+- Access: `$page.frontmatter.layout`
-当前页面的 `markdown` 文件中包裹在 `---` 中的配置,一般用于做一些页面级别的配置。
+当前页面所使用的布局组件名。
-::: tip
-VuePress 的动态布局系统等特性是基于 `frontmatter` 实现的,你可以使用插件 API [extendPageData](../plugin/option-api.md#extendpagedata) 在构建期间动态修改 frontmatter 的值。
-:::
+## frontmatter
+
+- Access: `$page.frontmatter`
+
+当前页面的 `markdown` 文件中包裹在 `---` 中的配置,一般用于做一些页面级别的配置,参考 [Front Matter](../guide/frontmatter.md) 一节了解更多。
## permalink
-> Access: `$page.frontmatter.permalink`
+- Access: `$page.frontmatter.permalink`
-永久链接,参考 [permalinks](../guide/permalinks.md) 了解更多。
+永久链接,参考 [Permalinks](../guide/permalinks.md) 一节了解更多。
## regularPath
-> Access: `$page.regularPath`
+- Access: `$page.regularPath`
当前页面基于目录结构生成的 URL。
-::: tip
-在构建期动态生成路由时,一个页面的 URL (`$page.path`) 将优先使用 `$page.frontmatter.permalink`,若不存在则降级到 `$page.regularPath`。
-:::
+## path
+
+- Access: `$page.path`
+
+当前页面的实际 URL。在构建期生成路由时,一个页面的 URL 将优先使用 `permalink`,若不存在则降级到 `regularPath`。
## headers
-> Access: `$page.headers`
+- Access: `$page.headers`
即 `markdown` 中那些以一个或多个 `#` 定义的标题。
## siteConfig
-> Access: `$site | Context.siteConfig`
+- Access: `$site | Context.siteConfig`
-即 `.vuepress/config.js`,译为`站点配置`。
+即 `.vuepress/config.js`,译为 `站点配置`。
## themeConfig
-> Access: `$site | Context.themeConfig`
+- Access: `$themeConfig | Context.themeConfig`
-即 `.vuepress/config.js` 中 `themeConfig` 的值,译为`用户的主题配置`。
+即 `.vuepress/config.js` 中 `themeConfig` 的值,是用户对当前所使用的主题的配置。
## themePath
-> Access: `Context.themePath`
+- Access: `Context.themeAPI.theme.path`
-当前使用的主题的根路径(绝对路径)。
+当前使用的主题的所在的绝对路径。
-## themeEntryFile
+## themeEntry
-> Access: `Context.themeEntryFile`
+- Access: `Context.themeAPI.theme.entry`
-主题的配置文件 (`themePath/index.js`)。
+主题的配置文件 `themePath/index.js`。
-## layout
+## parentThemePath
-> Access: `$page.frontmatter.layout`
+- Access: `Context.themeAPI.parentTheme.path`
+
+如果当前使用的主题是一个派生主题,那么 `parentThemePath` 就是指父主题的所在绝对路径。
+
+## parentThemeEntry
+
+- Access: `Context.themeAPI.parentTheme.entry`
+
+如果当前使用的主题是一个派生主题,那么 `parentThemePath` 就是指父主题的主题的配置文件 `parentThemePath/index.js`。
-当前页面所使用的布局组件名。
diff --git a/packages/docs/docs/zh/plugin/context-api.md b/packages/docs/docs/zh/plugin/context-api.md
index dfee98d9df..af72f8d015 100644
--- a/packages/docs/docs/zh/plugin/context-api.md
+++ b/packages/docs/docs/zh/plugin/context-api.md
@@ -42,12 +42,6 @@ VuePress 是否运行在生产环境模式下。
输出目录。
-## ctx.themePath
-
-- 类型: `string`
-
-当前应用的主题的根路径。
-
## ctx.base
- 类型: `string`
diff --git a/packages/docs/docs/zh/plugin/option-api.md b/packages/docs/docs/zh/plugin/option-api.md
index e92d839814..2ea8e80c0d 100644
--- a/packages/docs/docs/zh/plugin/option-api.md
+++ b/packages/docs/docs/zh/plugin/option-api.md
@@ -121,7 +121,7 @@ module.exports = (options, context) => ({
```js
module.exports = (options, context) => ({
chainWebpack (config) {
- config.resolve.alias.set('@theme', context.themePath)
+ config.resolve.alias.set('@pwd', process.cwd())
}
})
```
@@ -131,7 +131,7 @@ module.exports = (options, context) => ({
```js
module.exports = (options, context) => ({
alias: {
- '@theme': context.themePath
+ '@theme': context.themeAPI.themePath
}
})
```
diff --git a/packages/docs/docs/zh/theme/README.md b/packages/docs/docs/zh/theme/README.md
index 88856355e2..83447d4210 100644
--- a/packages/docs/docs/zh/theme/README.md
+++ b/packages/docs/docs/zh/theme/README.md
@@ -3,3 +3,11 @@
::: tip
主题组件受到相同的 [浏览器的 API 访问限制](../guide/using-vue.md#浏览器的API访问限制).
:::
+
+本栏的主要内容如下:
+
+- [使用主题](./using-a-theme.md)
+- [开发主题](./writing-a-theme.md)
+- [主题的配置](./option-api.md)
+- [主题的继承](./inheritance.md)
+- [默认主题配置](./default-theme-config.md)
diff --git a/packages/docs/docs/zh/theme/inheritance.md b/packages/docs/docs/zh/theme/inheritance.md
new file mode 100755
index 0000000000..cc0816b36b
--- /dev/null
+++ b/packages/docs/docs/zh/theme/inheritance.md
@@ -0,0 +1,181 @@
+# 主题的继承
+
+## 动机
+
+我们有两个主要的理由来支持这个特性:
+
+1. VuePress 为开发者提供了一个[默认主题](./default-theme-config.md),它能在大多数场景下满足了文档编写者的需求。即便如此,仍然还是会有不少用户选择将其 eject 出来进行修改,即使他们可能只是想要修改其中的某个组件。
+2. 在 [0.x](https://vuepress.vuejs.org/guide/custom-themes.html#site-and-page-metadata) 中,主题的入口只需要一个 `Layout.vue`,所以我们可以通过包装另一个主题的 `Layout.vue` 来实现简单的拓展。
+
+ 到了 1.x 中,一个主题的元素开始变得复杂,我们开始有了[主题级别的配置](./option-api.md),它支持为主题添加插件、自定义 GlobalLayout 等。除此之外,我们还有一些引入了主题开发的 [目录结构的约定](./writing-a-theme.md#目录结构),如 `styles/index.styl`,在这样的背景下,我们无法使用 0.x 的方式来实现继承了。
+
+因此,我们需要提供一种合理、可靠的主题继承方案。
+
+## 概念
+
+为了介绍本节,我们先几个基本概念:
+
+- **原子主题**:即父主题,类似默认主题这种完全从头实现的主题。
+- **派生主题**:即子主题,基于父主题创建的主题;
+
+::: tip 提示
+主题继承暂时不支持高阶继承,也就是说,一个派生主题无法被继承。
+:::
+
+## 使用
+
+假设你想创建一个继承自 VuePress 默认主题的派生主题,你只需要在你的主题配置中配置 [extend](./option-api.md#extend) 选项:
+
+```js
+module.exports = {
+ extend: '@vuepress/theme-default'
+}
+```
+
+## 继承策略
+
+父主题的所有能力都会"传递"给子主题,对于文件级别的约定,子主题可以通过在同样的位置创建同名文件来覆盖它;对于某些主题配置选项,如 [globalLayout](./option-api.md#globallayout),子主题也可以通过同名配置来覆盖它。
+
+[文件级别的约定](./writing-a-theme.md#目录结构)如下:
+
+- **全局组件**,即放置在 `theme/global-components` 中的 Vue 组件。
+- **组件**,即放置在 `theme/components` 中的 Vue 组件。
+- **全局的样式和调色板**,即放置在 `theme/styles` 中的 `index.styl` 和 `palette.styl`。
+- **HTML 模板**,即放置在 `theme/templates` 中的 `dev.html` 和 `ssr.html`。
+- **主题水平的客户端增强文件**,即 `theme/enhanceApp.js`
+
+对于主题配置,能被子主题覆盖的配置选项如下:
+
+- [devTemplate](./option-api.md#devtemplate)
+- [ssrTemplate](./option-api.md#ssrtemplate)
+- [globalLayout](./option-api.md#globallayout)
+
+无法被子主题覆盖的主题配置选项如下:
+
+- [extend](./option-api.md#extend)
+
+需要特殊处理的主题选项:
+
+- [plugins](./option-api.md#plugins):参见 [插件的覆盖](#插件的覆盖)。
+
+## 插件的覆盖
+
+对于父主题中的 [plugins](./option-api.md#plugins), 子主题不会直接覆盖它,但是插件的选项可以通过创建同名的插件配置来覆盖。
+
+举例来说,如果父主题具有如下配置:
+
+```js
+// parentThemePath/index.js
+module.exports = {
+ plugins: [
+ ['@vuepress/search', {
+ searchMaxSuggestions: 5
+ }]
+ ]
+}
+```
+
+那么子主题可以通过如下方式来修改该插件的默认值:
+
+```js
+// themePath/index.js
+module.exports = {
+ plugins: [
+ ['@vuepress/search', {
+ searchMaxSuggestions: 10
+ }]
+ ]
+}
+```
+
+也可以选择禁用它:
+
+```js
+// themePath/index.js
+module.exports = {
+ plugins: [
+ ['@vuepress/search', false]
+ ]
+}
+```
+
+::: warning
+一般情况下,你都不需要这样做,除非你明确知道禁用父主题中的插件不会带来问题。
+:::
+
+## 组件的覆盖
+
+你可能想要在子主题中覆盖父主题中的同名组件,默认情况下,当父主题中的组件都使用相对路径引用其他组件时,你将不可能做到这一点,因为你无法在运行时修改父主题的代码。
+
+VuePress 则通过一种巧妙的方式实现了这种需求,但这对父主题有一定的要求——**所有的组件都必须使用 `@theme` 别名来引用其他组件**。
+
+举例来说,如果你正在开发的一个原子主题的结构如下:
+
+::: vue
+theme
+├── components
+│ ├── `Home.vue`
+│ ├── `Navbar.vue`
+│ └── `Sidebar.vue`
+├── layouts
+│ ├── `404.vue`
+│ └── `Layout.vue`
+├── package.json
+└── index.js
+:::
+
+那么,在该主题中的任意 Vue 组件中,**你都应该通过 `@theme` 来访问主题根目录**:
+
+```vue
+
+```
+
+在这样的前提下,当你在子主题中同样的位置创建一个 `Navbar` 组件时:
+
+::: vue
+theme
+└── components
+ └── `Navbar.vue`
+:::
+
+`@theme/components/Navbar.vue` 会自动地映射到子主题中的 Navbar 组件,当你移除这个组件时,`@theme/components/Navbar.vue` 又会自动恢复为父主题中的 Navbar 组件。
+
+如此一来,就可以实现轻松地“篡改”一个原子主题的某个部分。
+
+::: tip
+1. 组件的覆盖,最好直接基于父主题中对应组件的代码来修改;
+2. 目前,在本地开发子主题,每次创建或移除组件时,你需要手动重启 Dev Server。
+:::
+
+## 访问父主题
+
+你可以使用 `@parent-theme` 来访问父主题的根路径,下述示例展示了在子主题中创建一个同名的布局组件,并简单使用父主题中的 slot,[@vuepress/theme-vue](https://github.com/vuejs/vuepress/tree/master/packages/%40vuepress/theme-vue) 便是通过这种方式创造的。
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+
+
diff --git a/packages/docs/docs/zh/theme/option-api.md b/packages/docs/docs/zh/theme/option-api.md
old mode 100644
new mode 100755
index f304f7bd12..3fb8ccd359
--- a/packages/docs/docs/zh/theme/option-api.md
+++ b/packages/docs/docs/zh/theme/option-api.md
@@ -1,8 +1,31 @@
---
-metaTitle: Option API | Theme
+metaTitle: Configuration | Theme
---
-# Option API
+# 主题的配置
+
+和插件几乎一样,主题的配置文件 `themeEntry` 应该导出一个普通的 JavaScript 对象(`#1`),它也可以是一个返回对象的函数(`#2`),这个函数接受用户在 `siteConfig.themeConfig` 为第一个参数、包含编译期上下文的 [ctx](./context-api.md) 对象作为第二个参数。
+
+``` js
+// #1
+module.exports = {
+ // ...
+}
+```
+
+``` js
+// #2
+module.exports = (themeConfig, ctx) => {
+ return {
+ // ...
+ }
+}
+```
+
+::: tip
+1. 你应该能看到 `themeEntry` 和 `themeConfig` 的区别,前者是一个主题本身的配置,这些配置由 VuePress 本身提供;而后者则是用户对主题的配置,这些配置选项则由当前使用的主题来实现,如 [默认主题配置](./default-theme-config.md)。
+2. 除了本节列出的选项,`themeEntry` 也支持插件支持的所有 [配置选项](../plugin/option-api.md) 和 [生命周期](../plugin/life-cycle.md)。
+:::
## plugins
@@ -13,11 +36,89 @@ metaTitle: Option API | Theme
- [插件 > 使用插件](../plugin/using-a-plugin.md).
-VuePress 支持一个主题继承于另一个主题。VuePress 将遵循 `override` 的方式自动帮你解决各种主题属性(如样式、布局组件)的优先级。
+---
+
+::: warning 注意
+你可能不需要使用下面这些带有 的选项,除非你知道你在做什么!
+:::
+
+## devTemplate
+
+- 类型: `String`
+- 默认值: undefined
+
+dev 模式下使用的 HTML 模板路径,默认模板见 [这里](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/index.dev.html)。
+
+## ssrTemplate
+
+- 类型: `String`
+- 默认值: undefined
+
+build 模式下使用的 HTML 模板路径,默认模板见 [这里](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/index.ssr.html)。
+
+**参考:**
+
+- [Vue SSR Guide > template](https://ssr.vuejs.org/zh/api/#createrenderer).
+
+
+## extend
-值得注意的是,在子主题中,VuePress 将注入一个指向父主题包目录根路径的 [alias](../plugin/option-api.md#alias) `@parent-theme`。
+- 类型: `String`
+- 默认值: undefined
+
+```js
+module.exports = {
+ extend: '@vuepress/theme-default'
+}
+```
+
+VuePress 支持一个主题继承于另一个主题。VuePress 将遵循 `override` 的理念自动帮你解决各种主题属性(如样式、布局组件)的优先级。
**参考:**
+- [主题继承](./inheritance.md)
- [例子: `@vuepress/theme-vue`](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/theme-vue)
-- [Design Concepts of VuePress 1.x](../miscellaneous/design-concepts.md)
+
+## globalLayout
+
+- 类型: `String`
+- 默认值: undefined
+
+```js
+// themePath/index.js
+module.exports = {
+ globalLayout: '/path/to/your/global/vue/sfc'
+}
+```
+
+全局布局组件是负责管理全局布局方案的一个组件,VuePress [默认的 globalLayout](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/components/GlobalLayout.vue)会帮你根据 [$frontmatter.layout](../guide/frontmatter.md#layout) 来渲染不同的布局,所以大部分情况下你不要配置此选项。
+
+举例来说,当你想为当前主题设置全局的 header 和 footer 时,你可以这样做:
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+```
diff --git a/packages/docs/docs/zh/theme/using-a-theme.md b/packages/docs/docs/zh/theme/using-a-theme.md
index 3212fd82ea..2c4f729db8 100644
--- a/packages/docs/docs/zh/theme/using-a-theme.md
+++ b/packages/docs/docs/zh/theme/using-a-theme.md
@@ -1,10 +1,10 @@
# 使用主题
-使用一个主题和使用一个插件几乎一致。
+使用一个主题和使用一个插件的方式几乎一致。
-## 使用 dependency 中的主题
+## 使用来自依赖的主题
-一个插件可以在以 `vuepress-theme-xxx` 的形式发布到 npm,你可以这样使用它:
+一个主题可以在以 `vuepress-theme-xxx` 的形式发布到 npm,你可以这样使用它:
``` js
module.exports = {
@@ -14,7 +14,7 @@ module.exports = {
## 主题的缩写
-如果你的插件名以 `vuepress-theme-` 开头,你可以使用缩写来省略这个前缀:
+如果你的主题名以 `vuepress-theme-` 开头,你可以使用缩写来省略这个前缀:
``` js
module.exports = {
@@ -47,5 +47,5 @@ module.exports = {
```
::: warning 注意
-以 `@vuepress/plugin-` 开头的插件是官方维护的插件。
+以 `@vuepress/theme-` 开头的主题是官方维护的主题。
:::
diff --git a/packages/docs/docs/zh/theme/writing-a-theme.md b/packages/docs/docs/zh/theme/writing-a-theme.md
old mode 100644
new mode 100755
index 0c586cb115..a40b42d907
--- a/packages/docs/docs/zh/theme/writing-a-theme.md
+++ b/packages/docs/docs/zh/theme/writing-a-theme.md
@@ -33,27 +33,27 @@
## 目录结构
-随着需求的变化,只有一个布局组件 `Layout.vue` 可能还不够,你可能想要定义更多的布局组件用于不同的页面,你可能还想要自定义一个[调色板](../config/README.md#palette-styl), 甚至应用一些插件。
+随着需求的变化,只有一个布局组件 `Layout.vue` 可能还不够,你可能想要定义更多的布局组件用于不同的页面,你可能还想要自定义一个[调色板](../config/README.md#palette-styl),甚至应用一些插件。
那么是时候重新组织你的主题了!一个约定的主题的目录结构如下:
::: vue
-themePath
-├── `global-components` _(**可选的**)_
+theme
+├── `global-components`
│ └── xxx.vue
-├── `components` _(**可选的**)_
+├── `components`
│ └── xxx.vue
├── `layouts`
│ ├── Layout.vue _(**必要的**)_
-│ └── 404.vue _(**可选的**)_
-├── `styles` _(**必要的**)_
+│ └── 404.vue
+├── `styles`
│ ├── index.styl
│ └── palette.styl
-├── `templates` _(**必要的**)_
+├── `templates`
│ ├── dev.html
│ └── ssr.html
-├── `index.js` _(**当你将主题发布为一个 npm 包时,这是必须的!**)_
-├── `enhanceApp.js` _(**必要的**)_
+├── `index.js`
+├── `enhanceApp.js`
└── package.json
:::
@@ -66,12 +66,12 @@ themePath
- `theme/enhanceApp.js`: 主题水平的客户端增强文件。
::: warning 注意
-当你将你的主题以一个 npm 包的形式发布时,请确保这个包包含 `index.js`,同时将 `package.json` 中的 `"main"` 字段设置为 `index.js`,如此一来 VuePress 才能获取到正确的 [themePath](../miscellaneous/glossary.md#theme-side).
+当你将你的主题以一个 npm 包的形式发布时,如果你没有任何主题配置,即没有 `theme/index.js`,那么你需要将 `package.json` 中的 `"main"` 字段设置为 `layouts/Layout.vue`,只有这样 VuePress 才能正确地解析主题。
```json
{
...
- "main": "index.js"
+ "main": "layouts/Layout.vue",
...
}
```
@@ -100,6 +100,10 @@ layout: AnotherLayout
---
````
+::: tip
+每个 layout 组件都可能会渲染出截然不同的页面,如果你想设置一些全局的 UI(如全局的 ``),可以考虑使用 [globalLayout](./option-api.md#globallayout)。
+:::
+
## 使用插件
你可以通过主题的配置文件 `themePath/index.js` 来给主题应用一些插件: