Skip to content

Commit 5df8662

Browse files
committed
feat(core): support multi-level theme inheritance
BREAKING CHANGE: `app.themeApi` is removed
1 parent 4c6c1a9 commit 5df8662

37 files changed

+238
-352
lines changed

docs/reference/theme-api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ module.exports = {
8888

8989
If a layout with the same name is registered in both the child theme and the parent theme, the layout of the child theme will have a higher priority.
9090

91-
Multi-level inheritance is not supported.
91+
Multi-level inheritance is supported.
9292

9393
- Example:
9494

docs/zh/reference/theme-api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ module.exports = {
8888

8989
如果在子主题和父主题中都注册了具有相同名称的布局,则子主题的布局将具有更高的优先级。
9090

91-
不支持多级继承
91+
支持多级继承
9292

9393
- 示例:
9494

packages/@vuepress/core/__tests__/__fixtures__/layouts/Bar.vue

Whitespace-only changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const { path } = require('@vuepress/utils')
2+
3+
module.exports = {
4+
name: 'theme-has-grandparent',
5+
extends: path.resolve(__dirname, './has-parent'),
6+
layouts: {
7+
404: path.resolve(__dirname, '../layouts/Bar.vue'),
8+
Bar: path.resolve(__dirname, '../layouts/Bar.vue'),
9+
},
10+
}

packages/@vuepress/core/__tests__/__fixtures__/themes/has-parent.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module.exports = {
44
name: 'theme-has-parent',
55
extends: path.resolve(__dirname, './has-layouts'),
66
layouts: {
7+
404: path.resolve(__dirname, '../layouts/Foo.vue'),
78
Foo: path.resolve(__dirname, '../layouts/Foo.vue'),
89
},
910
}

packages/@vuepress/core/__tests__/app/createAppPages.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ describe('core > app > createAppPages', () => {
55
it('should create two pages with default 404 page', async () => {
66
const app = createApp({
77
source: path.resolve(__dirname, '../__fixtures__/pages'),
8+
theme: path.resolve(__dirname, '../__fixtures__/themes/no-layouts.js'),
89
})
910

1011
const pages = await createAppPages(app)
@@ -22,6 +23,7 @@ describe('core > app > createAppPages', () => {
2223
it('should create two pages with custom 404 page', async () => {
2324
const app = createApp({
2425
source: path.resolve(__dirname, '../__fixtures__/pages-with-404'),
26+
theme: path.resolve(__dirname, '../__fixtures__/themes/no-layouts.js'),
2527
})
2628

2729
const pages = await createAppPages(app)
@@ -38,6 +40,7 @@ describe('core > app > createAppPages', () => {
3840
it('should process extendsPageOptions hook correctly', async () => {
3941
const app = createApp({
4042
source: path.resolve(__dirname, '../__fixtures__/pages-with-404'),
43+
theme: path.resolve(__dirname, '../__fixtures__/themes/no-layouts.js'),
4144
})
4245

4346
app.use({

packages/@vuepress/core/__tests__/app/normalizePlugin.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { createApp, normalizePlugin } from '@vuepress/core'
22
import type { PluginFunction, PluginObject } from '@vuepress/core'
33
import { path } from '@vuepress/utils'
44

5-
const source = path.resolve(__dirname, 'fake-source')
65
const app = createApp({
7-
source,
6+
source: path.resolve(__dirname, 'fake-source'),
7+
theme: path.resolve(__dirname, '../__fixtures__/themes/no-layouts.js'),
88
})
99

1010
describe('core > app > normalizePlugin', () => {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { createApp, resolveTheme } from '@vuepress/core'
2+
import { path } from '@vuepress/utils'
3+
4+
const fixtures = (...args: string[]) =>
5+
path.resolve(__dirname, '../__fixtures__/', ...args)
6+
7+
describe('core > app > resolveTheme', () => {
8+
it('should resolve theme without layouts correctly', () => {
9+
const app = createApp({
10+
source: path.resolve(__dirname, 'fake-source'),
11+
theme: fixtures('themes/no-layouts.js'),
12+
})
13+
14+
expect(resolveTheme(app, app.options.theme)).toEqual({
15+
plugins: [require(fixtures('themes/no-layouts.js'))],
16+
layouts: {},
17+
})
18+
})
19+
20+
it('should create theme api with layouts correctly', () => {
21+
const app = createApp({
22+
source: path.resolve(__dirname, 'fake-source'),
23+
theme: fixtures('themes/has-layouts.js'),
24+
})
25+
26+
expect(resolveTheme(app, app.options.theme)).toEqual({
27+
plugins: [require(fixtures('themes/has-layouts.js'))],
28+
layouts: {
29+
Layout: fixtures('layouts/Layout.vue'),
30+
404: fixtures('layouts/404.vue'),
31+
},
32+
})
33+
})
34+
35+
it('should create theme api with parent theme correctly', () => {
36+
const app = createApp({
37+
source: path.resolve(__dirname, 'fake-source'),
38+
theme: fixtures('themes/has-parent.js'),
39+
})
40+
41+
expect(resolveTheme(app, app.options.theme)).toEqual({
42+
plugins: [
43+
require(fixtures('themes/has-layouts.js')),
44+
require(fixtures('themes/has-parent.js')),
45+
],
46+
layouts: {
47+
Layout: fixtures('layouts/Layout.vue'),
48+
Foo: fixtures('layouts/Foo.vue'),
49+
404: fixtures('layouts/Foo.vue'),
50+
},
51+
})
52+
})
53+
54+
it('should create theme api with grandparent theme correctly', () => {
55+
const app = createApp({
56+
source: path.resolve(__dirname, 'fake-source'),
57+
theme: fixtures('themes/has-grandparent.js'),
58+
})
59+
60+
expect(resolveTheme(app, app.options.theme)).toEqual({
61+
plugins: [
62+
require(fixtures('themes/has-layouts.js')),
63+
require(fixtures('themes/has-parent.js')),
64+
require(fixtures('themes/has-grandparent.js')),
65+
],
66+
layouts: {
67+
Layout: fixtures('layouts/Layout.vue'),
68+
Foo: fixtures('layouts/Foo.vue'),
69+
Bar: fixtures('layouts/Bar.vue'),
70+
404: fixtures('layouts/Bar.vue'),
71+
},
72+
})
73+
})
74+
})

packages/@vuepress/core/__tests__/themeApi/resolveThemeLayouts.spec.ts renamed to packages/@vuepress/core/__tests__/app/resolveThemeLayouts.spec.ts

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,45 +9,31 @@ const testCases: [
99
ReturnType<typeof resolveThemeLayouts>
1010
][] = [
1111
// `layouts` is not provided
12-
[undefined, []],
12+
[undefined, {}],
1313
// `layouts` is an object
1414
[
1515
{
1616
Layout: fixtures('Layout.vue'),
1717
404: fixtures('404.vue'),
1818
},
19-
[
20-
{
21-
name: '404',
22-
path: fixtures('404.vue'),
23-
},
24-
{
25-
name: 'Layout',
26-
path: fixtures('Layout.vue'),
27-
},
28-
],
19+
{
20+
Layout: fixtures('Layout.vue'),
21+
404: fixtures('404.vue'),
22+
},
2923
],
3024
// `layouts` is an absolute path
3125
[
3226
fixtures(),
33-
[
34-
{
35-
name: '404',
36-
path: fixtures('404.vue'),
37-
},
38-
{
39-
name: 'Foo',
40-
path: fixtures('Foo.vue'),
41-
},
42-
{
43-
name: 'Layout',
44-
path: fixtures('Layout.vue'),
45-
},
46-
],
27+
{
28+
Layout: fixtures('Layout.vue'),
29+
Foo: fixtures('Foo.vue'),
30+
Bar: fixtures('Bar.vue'),
31+
404: fixtures('404.vue'),
32+
},
4733
],
4834
]
4935

50-
describe('core > themeApi > resolveThemeLayouts', () => {
36+
describe('core > app > resolveThemeLayouts', () => {
5137
describe('should resolve theme layouts correctly', () => {
5238
testCases.forEach(([source, expected]) => {
5339
it(`${typeof source}`, () => {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createApp, resolveThemePlugin } from '@vuepress/core'
2+
import { path } from '@vuepress/utils'
3+
4+
const fixtures = (...args: string[]) =>
5+
path.resolve(__dirname, '../__fixtures__/', ...args)
6+
const app = createApp({
7+
source: path.resolve(__dirname, 'fake-source'),
8+
theme: fixtures('themes/no-layouts.js'),
9+
})
10+
11+
describe('core > app > resolveThemePlugin', () => {
12+
it('should resolve theme plugin by absolute path correctly', () => {
13+
expect(resolveThemePlugin(app, fixtures('themes/no-layouts.js'))).toEqual(
14+
require(fixtures('themes/no-layouts.js'))
15+
)
16+
})
17+
18+
it('should throw an error if the theme path does not exist', () => {
19+
const consoleError = console.error
20+
console.error = jest.fn()
21+
22+
expect(() => {
23+
resolveThemePlugin(app, fixtures('themes/4-0-4.js'))
24+
}).toThrow()
25+
expect(console.error).toHaveBeenCalled()
26+
27+
console.error = consoleError
28+
})
29+
30+
it('should throw an error if the theme name does not exist', () => {
31+
const consoleError = console.error
32+
console.error = jest.fn()
33+
34+
expect(() => {
35+
resolveThemePlugin(app, '4-0-4')
36+
}).toThrow()
37+
expect(console.error).toHaveBeenCalled()
38+
39+
console.error = consoleError
40+
})
41+
})

0 commit comments

Comments
 (0)