Skip to content

Commit 84fd0ff

Browse files
committed
feat: extend a theme
1 parent 1a0be05 commit 84fd0ff

File tree

7 files changed

+124
-17
lines changed

7 files changed

+124
-17
lines changed

packages/@vuepress/core/lib/internal-plugins/palette/index.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,22 @@ module.exports = (options, ctx) => ({
2626
const themePaletteContent = fs.existsSync(themePalette)
2727
? `@import(${JSON.stringify(themePalette)})`
2828
: ''
29+
2930
const userPaletteContent = fs.existsSync(userPalette)
3031
? `@import(${JSON.stringify(userPalette)})`
3132
: ''
3233

3334
// user's palette can override theme's palette.
34-
const paletteContent = themePaletteContent + userPaletteContent
35+
let paletteContent = themePaletteContent + userPaletteContent
36+
37+
if (ctx.parentThemePath) {
38+
const parentThemePalette = path.resolve(ctx.parentThemePath, 'styles/palette.styl')
39+
const parentThemePaletteContent = fs.existsSync(parentThemePalette)
40+
? `@import(${JSON.stringify(parentThemePalette)})`
41+
: ''
42+
paletteContent = parentThemePaletteContent + paletteContent
43+
}
44+
3545
await writeTemp('palette.styl', paletteContent)
3646
}
3747
})

packages/@vuepress/core/lib/internal-plugins/style/index.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,21 @@ module.exports = (options, ctx) => ({
2121
const themeStyleContent = fs.existsSync(themeStyle)
2222
? `@import(${JSON.stringify(themeStyle)})`
2323
: ''
24+
2425
const userStyleContent = fs.existsSync(userStyle)
2526
? `@import(${JSON.stringify(userStyle)})`
2627
: ''
2728

28-
const styleContent = themeStyleContent + userStyleContent
29+
let styleContent = themeStyleContent + userStyleContent
30+
31+
if (ctx.parentThemePath) {
32+
const parentThemeStyle = path.resolve(ctx.parentThemePath, 'styles/index.styl')
33+
const parentThemeStyleContent = fs.existsSync(parentThemeStyle)
34+
? `@import(${JSON.stringify(parentThemeStyle)})`
35+
: ''
36+
styleContent = parentThemeStyleContent + styleContent
37+
}
38+
2939
await writeTemp('style.styl', styleContent)
3040
}
3141
})

packages/@vuepress/core/lib/prepare/AppContext.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ module.exports = class AppContext {
128128
.use('@vuepress/register-components', {
129129
componentsDir: [
130130
path.resolve(this.sourceDir, '.vuepress/components'),
131-
path.resolve(this.themePath, 'global-components')
131+
path.resolve(this.themePath, 'global-components'),
132+
this.parentThemePath && path.resolve(this.parentThemePath, 'global-components')
132133
]
133134
})
134135
}
@@ -140,8 +141,11 @@ module.exports = class AppContext {
140141
*/
141142

142143
applyUserPlugins () {
144+
this.pluginAPI.useByPluginsConfig(this.cliOptions.plugins)
145+
if (this.parentThemePath) {
146+
this.pluginAPI.use(this.parentThemeEntryFile)
147+
}
143148
this.pluginAPI
144-
.useByPluginsConfig(this.cliOptions.plugins)
145149
.use(this.themeEntryFile)
146150
.use(Object.assign({}, this.siteConfig, { name: '@vuepress/internal-site-config' }))
147151
}
@@ -193,20 +197,25 @@ module.exports = class AppContext {
193197
const themeSsrTemplate = path.resolve(this.themePath, 'templates/ssr.html')
194198
const themeDevTemplate = path.resolve(this.themePath, 'templates/dev.html')
195199

200+
const parentThemeSsrTemplate = path.resolve(this.themePath, 'templates/ssr.html')
201+
const parentThemeDevTemplate = path.resolve(this.themePath, 'templates/dev.html')
202+
196203
const defaultSsrTemplate = path.resolve(__dirname, '../app/index.ssr.html')
197204
const defaultDevTemplate = path.resolve(__dirname, '../app/index.dev.html')
198205

199206
const ssrTemplate = fsExistsFallback([
200207
siteSsrTemplate,
201208
siteSsrTemplate2,
202209
themeSsrTemplate,
210+
parentThemeSsrTemplate,
203211
defaultSsrTemplate
204212
])
205213

206214
const devTemplate = fsExistsFallback([
207215
siteDevTemplate,
208216
siteDevTemplate2,
209217
themeDevTemplate,
218+
parentThemeDevTemplate,
210219
defaultDevTemplate
211220
])
212221

packages/@vuepress/core/lib/prepare/loadTheme.js

+52-10
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,21 @@ module.exports = async function loadTheme (ctx) {
4343
let themeEntryFile = null // Optional
4444
let themeName
4545
let themeShortcut
46+
let parentThemePath = null // Optional
47+
let parentThemeEntryFile = null // Optional
4648

4749
if (useLocalTheme) {
4850
themePath = localThemePath
4951
logger.tip(`\nApply theme located at ${chalk.gray(themePath)}...`)
5052
} else if (isString(theme)) {
5153
const resolved = themeResolver.resolve(theme, sourceDir)
52-
const { entry: modulePath, name, shortcut } = resolved
53-
if (modulePath === null) {
54+
const { entry, name, shortcut } = resolved
55+
56+
if (entry === null) {
5457
throw new Error(`Cannot resolve theme ${theme}.`)
5558
}
56-
if (modulePath.endsWith('.js') || modulePath.endsWith('.vue')) {
57-
themePath = path.parse(modulePath).dir
58-
} else {
59-
themePath = modulePath
60-
}
59+
60+
themePath = normalizeThemePath(resolved)
6161
themeName = name
6262
themeShortcut = shortcut
6363
logger.tip(`\nApply theme ${chalk.gray(themeName)}`)
@@ -70,15 +70,43 @@ module.exports = async function loadTheme (ctx) {
7070
} catch (error) {
7171
themeEntryFile = {}
7272
}
73+
7374
themeEntryFile.name = '@vuepress/internal-theme-entry-file'
7475
themeEntryFile.shortcut = null
7576

7677
// handle theme api
7778
const layoutDirs = [
78-
path.resolve(themePath, 'layouts'),
79-
path.resolve(themePath, '.')
79+
path.resolve(themePath, '.'),
80+
path.resolve(themePath, 'layouts')
8081
]
8182

83+
if (themeEntryFile.extend) {
84+
const resolved = themeResolver.resolve(themeEntryFile.extend, sourceDir)
85+
if (resolved.entry === null) {
86+
throw new Error(`Cannot resolve parent theme ${themeEntryFile.extend}.`)
87+
}
88+
parentThemePath = normalizeThemePath(resolved)
89+
90+
try {
91+
parentThemeEntryFile = pluginAPI.normalizePlugin(parentThemePath, ctx.themeConfig)
92+
} catch (error) {
93+
parentThemeEntryFile = {}
94+
}
95+
96+
parentThemeEntryFile.name = '@vuepress/internal-parent-theme-entry-file'
97+
parentThemeEntryFile.shortcut = null
98+
99+
layoutDirs.unshift(
100+
path.resolve(parentThemePath, '.'),
101+
path.resolve(parentThemePath, 'layouts'),
102+
)
103+
104+
themeEntryFile.alias = Object.assign(
105+
themeEntryFile.alias || {},
106+
{ '@parent-theme': parentThemePath }
107+
)
108+
}
109+
82110
// normalize component name
83111
const getComponentName = filename => {
84112
filename = filename.slice(0, -4)
@@ -139,6 +167,20 @@ module.exports = async function loadTheme (ctx) {
139167
layoutComponentMap,
140168
themeEntryFile,
141169
themeName,
142-
themeShortcut
170+
themeShortcut,
171+
parentThemePath,
172+
parentThemeEntryFile
173+
}
174+
}
175+
176+
function normalizeThemePath (resolved) {
177+
const { entry, name, fromDep } = resolved
178+
if (fromDep) {
179+
const pkgPath = require.resolve(`${name}/package.json`)
180+
return path.parse(pkgPath).dir
181+
} else if (entry.endsWith('.js') || entry.endsWith('.vue')) {
182+
return path.parse(entry).dir
183+
} else {
184+
return entry
143185
}
144186
}

packages/@vuepress/plugin-register-components/index.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { fs, path, globby } = require('@vuepress/shared-utils')
1+
const { fs, path, globby, datatypes: { isString }} = require('@vuepress/shared-utils')
22

33
function fileToComponentName (file) {
44
return file
@@ -40,6 +40,9 @@ module.exports = (options, context) => ({
4040

4141
// 1. Register components in specified directories
4242
for (const baseDir of baseDirs) {
43+
if (!isString(baseDir)) {
44+
continue
45+
}
4346
const files = await resolveComponents(baseDir) || []
4447
code += files.map(file => genImport(baseDir, file)).join('\n') + '\n'
4548
}

packages/docs/docs/theme/option-api.md

+23-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,26 @@ seoTitle: Option API | Theme
99
- Type: `Array|Object`
1010
- Default: undefined
1111

12-
See: [Plugin > Using a plugin](../plugin/using-a-plugin.md).
12+
**Also see:**
13+
14+
- [Plugin > Using a plugin](../plugin/using-a-plugin.md).
15+
16+
## extend
17+
18+
- Type: `String`
19+
- Default: undefined
20+
21+
```js
22+
module.exports = {
23+
extend: '@vuepress/theme-default'
24+
}
25+
```
26+
27+
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.
28+
29+
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.
30+
31+
**Also see:**
32+
33+
- [Example: `@vuepress/theme-vue`](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/theme-vue)
34+
- [Design Concepts of VuePress 1.x](../miscellaneous/design-concepts.md)

packages/docs/docs/zh/theme/option-api.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,15 @@ seoTitle: Option API | Theme
99
- 类型: `Array|Object`
1010
- 默认值: undefined
1111

12-
参考: [插件 > 使用插件](../plugin/using-a-plugin.md).
12+
**参考:**
13+
14+
- [插件 > 使用插件](../plugin/using-a-plugin.md).
15+
16+
VuePress 支持一个主题继承于另一个主题。VuePress 将遵循 `override` 的方式自动帮你解决各种主题属性(如样式、布局组件)的优先级。
17+
18+
值得注意的是,在子主题中,VuePress 将注入一个指向父主题包目录根路径的 [alias](../plugin/option-api.md#alias) `@parent-theme`
19+
20+
**参考:**
21+
22+
- [例子: `@vuepress/theme-vue`](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/theme-vue)
23+
- [Design Concepts of VuePress 1.x](../miscellaneous/design-concepts.md)

0 commit comments

Comments
 (0)