Skip to content

Commit 7f7fe18

Browse files
committed
feat: support clean and multiple custom layout. / theme default layouts dir
1. Refine ExtendPageDataOption, simplify the implmentation 2. Introduce LayoutDistributor as the unique route layout entry 3. Inject layout components to LayoutDistributor 4. Register layout components located at themedir/layouts 5. Reorganize the default theme's structure
1 parent e08cc2e commit 7f7fe18

40 files changed

+189
-102
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<template>
2+
<component :is="layout"/>
3+
</template>
4+
5+
<script>
6+
export default {
7+
computed: {
8+
layout () {
9+
if (this.$page.path) {
10+
return this.$page.frontmatter.layout || 'Layout'
11+
}
12+
return 'NotFound'
13+
}
14+
}
15+
}
16+
</script>

packages/@vuepress/core/lib/app/util.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
import Vue from 'vue'
22
import { loadComponent } from '@internal/async-component'
33

4-
export function injectMixins (options, mixins) {
5-
if (!options.mixins) {
6-
options.mixins = []
4+
/**
5+
* Inject option to Vue SFC
6+
* @param {object} options
7+
* @param {string} key
8+
* @param {any} value
9+
*/
10+
export function injectComponentOption (options, key, value) {
11+
const arrayInject = () => {
12+
if (!options[key]) options[key] = []
13+
options[key].push(...value)
14+
}
15+
const objectInject = () => {
16+
if (!options[key]) options[key] = {}
17+
Object.assign(options[key], value)
18+
}
19+
// const primitiveInject = () => options[key] = value
20+
21+
switch (key) {
22+
case 'components': objectInject(); break
23+
case 'mixins': arrayInject(); break
24+
default: throw new Error('Unknown option name.')
725
}
8-
options.mixins.push(...mixins)
926
}
1027

1128
export function findPageForPath (pages, path) {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module.exports = (options, ctx) => {
2+
const { layoutComponentMap } = ctx
3+
const componentNames = Object.keys(layoutComponentMap)
4+
5+
return {
6+
name: '@vuepress/internal-layout',
7+
8+
async clientDynamicModules () {
9+
const code = `export default {\n${componentNames
10+
.map(name => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(layoutComponentMap[name].path)})`)
11+
.join(',\n')} \n}`
12+
return { name: 'layout-components.js', content: code, dirname: 'internal' }
13+
},
14+
15+
chainWebpack (config, isServer) {
16+
const setAlias = (alias, raw) => config.resolve.alias.set(alias, raw)
17+
componentNames.forEach(name => {
18+
setAlias(`@${name}`, layoutComponentMap[name].path)
19+
})
20+
}
21+
}
22+
}

packages/@vuepress/core/lib/internal-plugins/routes.js

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
1-
module.exports = (options, context) => ({
1+
module.exports = (options, ctx) => ({
22
name: '@vuepress/internal-routes',
33

44
// @internal/routes
55
async clientDynamicModules () {
6-
const routesCode = await genRoutesFile(context.pages)
7-
return { name: 'routes.js', content: routesCode, dirname: 'internal' }
6+
const code = importCode() + routesCode(ctx.pages)
7+
return { name: 'routes.js', content: code, dirname: 'internal' }
88
}
99
})
1010

1111
/**
12-
* Generate routes meta data file.
13-
* @param pages
14-
* @returns {Promise<string>}
12+
* Import utilities
13+
* @returns {string}
1514
*/
16-
async function genRoutesFile (pages) {
15+
function importCode () {
16+
return `
17+
import { injectComponentOption, registerComponent } from '@app/util'
18+
import rootMixins from '@internal/root-mixins'
19+
import components from '@internal/layout-components'
20+
import LayoutDistributor from '@app/components/LayoutDistributor.vue'
21+
22+
injectComponentOption(LayoutDistributor, 'mixins', rootMixins)
23+
injectComponentOption(LayoutDistributor, 'components', components)
24+
`
25+
}
26+
27+
/**
28+
* Get Vue routes code.
29+
* @param {array} pages
30+
* @returns {string}
31+
*/
32+
function routesCode (pages) {
1733
function genRoute ({
1834
path: pagePath,
1935
key: componentName,
@@ -23,7 +39,7 @@ async function genRoutesFile (pages) {
2339
{
2440
name: ${JSON.stringify(componentName)},
2541
path: ${JSON.stringify(pagePath)},
26-
component: ThemeLayout,
42+
component: LayoutDistributor,
2743
beforeEnter: (to, from, next) => {
2844
registerComponent(${JSON.stringify(componentName)}).then(() => next())
2945
}
@@ -60,16 +76,10 @@ async function genRoutesFile (pages) {
6076
const notFoundRoute = `,
6177
{
6278
path: '*',
63-
component: ThemeNotFound
79+
component: LayoutDistributor
6480
}`
6581

6682
return (
67-
`import ThemeLayout from '@themeLayout'\n` +
68-
`import ThemeNotFound from '@themeNotFound'\n` +
69-
`import { injectMixins, registerComponent } from '@app/util'\n` +
70-
`import rootMixins from '@internal/root-mixins'\n\n` +
71-
`injectMixins(ThemeLayout, rootMixins)\n` +
72-
`injectMixins(ThemeNotFound, rootMixins)\n\n` +
7383
`export const routes = [${pages.map(genRoute).join(',')}${notFoundRoute}\n]`
7484
)
7585
}

packages/@vuepress/core/lib/plugin-api/option/ExtendPageDataOption.js

Lines changed: 0 additions & 12 deletions
This file was deleted.

packages/@vuepress/core/lib/plugin-api/option/instantiateOption.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
const EnhanceAppFilesOption = require('./EnhanceAppFilesOption')
2-
const ExtendPageDataOption = require('./ExtendPageDataOption')
32
const ClientDynamicModulesOption = require('./ClientDynamicModulesOption')
43
const AdditionalPagesOption = require('./AdditionalPagesOption')
54
const GlobalUIComponentsOption = require('./GlobalUIComponentsOption')
@@ -11,9 +10,6 @@ module.exports = function instantiateOption (name) {
1110
case PLUGIN_OPTION_MAP.ENHANCE_APP_FILES.name:
1211
return new EnhanceAppFilesOption(name)
1312

14-
case PLUGIN_OPTION_MAP.EXTEND_PAGE_DATA.name:
15-
return new ExtendPageDataOption(name)
16-
1713
case PLUGIN_OPTION_MAP.CLIENT_DYNAMIC_MODULES.name:
1814
return new ClientDynamicModulesOption(name)
1915

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,11 @@ module.exports = class AppContext {
5252
this.resolvePlugins()
5353

5454
await this.resolvePages()
55-
await this.pluginAPI.options.additionalPages.values.map(async ({ path, permalink }) => {
56-
await this.addPage(path, { permalink })
57-
})
55+
await Promise.all(
56+
this.pluginAPI.options.additionalPages.values.map(async ({ path, permalink }) => {
57+
await this.addPage(path, { permalink })
58+
})
59+
)
5860

5961
await this.pluginAPI.options.ready.apply()
6062
this.pluginAPI.options.extendMarkdown.syncApply(this.markdown)
@@ -85,6 +87,7 @@ module.exports = class AppContext {
8587
.use(require('../internal-plugins/enhanceApp'))
8688
.use(require('../internal-plugins/overrideCSS'))
8789
.use(require('../internal-plugins/i18nTemp'))
90+
.use(require('../internal-plugins/layouts'))
8891
// user plugin
8992
.useByPluginsConfig(this._options.plugins)
9093
.useByPluginsConfig(this.siteConfig.plugins)
@@ -153,13 +156,16 @@ module.exports = class AppContext {
153156
* @returns { Promise<void> }
154157
*/
155158
async addPage (filePath, { relative, permalink }) {
156-
const page = new Page(filePath, { relative, permalink })
159+
const page = new Page(filePath, {
160+
relative,
161+
permalink,
162+
permalinkPattern: this.siteConfig.permalink
163+
})
157164
await page.process(
158165
this.markdown,
159-
this.siteConfig.permalink,
160166
new this.I18nConstructor((this.getSiteData.bind(this))),
167+
this.pluginAPI.options.extendPageData.items
161168
)
162-
await this.pluginAPI.options.extendPageData.apply(page)
163169
this.pages.push(page)
164170
}
165171

@@ -184,6 +190,7 @@ module.exports = class AppContext {
184190
* }}
185191
*/
186192
getSiteData () {
193+
console.log('2')
187194
return {
188195
title: this.siteConfig.title || '',
189196
description: this.siteConfig.description || '',

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

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
const path = require('path')
22
const slugify = require('../markdown/slugify')
3-
const { fs, fileToPath, parseFrontmatter, getPermalink } = require('@vuepress/shared-utils')
43
const { inferTitle, extractHeaders } = require('../util/index')
4+
const {
5+
fs, fileToPath, parseFrontmatter, getPermalink,
6+
datatypes: { isPlainObject }
7+
} = require('@vuepress/shared-utils')
58

69
module.exports = class Page {
710
constructor (filePath, {
811
relative,
9-
permalink
12+
permalink,
13+
permalinkPattern
1014
}) {
1115
this._filePath = filePath
1216
if (relative) {
@@ -15,50 +19,67 @@ module.exports = class Page {
1519
this._routePath = encodeURI(permalink)
1620
}
1721
this.regularPath = this.path = this._routePath
22+
this._permalinkPattern = permalinkPattern
1823
}
1924

20-
async process (markdown, permalinkPattern, i18n) {
25+
async process (markdown, i18n, enhancers) {
2126
this.key = 'v-' + Math.random().toString(16).slice(2)
2227
this._content = await fs.readFile(this._filePath, 'utf-8')
23-
const frontmatter = parseFrontmatter(this._content)
28+
this._frontmatterMeta = parseFrontmatter(this._content)
2429

2530
// infer title
26-
const title = inferTitle(frontmatter)
31+
const title = inferTitle(this._frontmatterMeta)
2732
if (title) {
2833
this.title = title
2934
}
3035

3136
// headers
3237
const headers = extractHeaders(
33-
frontmatter.content,
38+
this._frontmatterMeta.content,
3439
['h2', 'h3'],
3540
markdown
3641
)
3742
if (headers.length) {
3843
this.headers = headers
3944
}
4045

41-
this.frontmatter = frontmatter.data
46+
for (const { name: pluginName, value: enhancer } of enhancers) {
47+
let response
48+
try {
49+
response = enhancer(this)
50+
} catch (error) {
51+
throw new Error(`[${pluginName}] excuete extendPageData failed.`)
52+
}
53+
if (isPlainObject(response)) {
54+
Object.keys(response).forEach(key => {
55+
if (!this[key]) {
56+
this[key] = response[key]
57+
}
58+
})
59+
}
60+
}
61+
62+
this.frontmatter = this._frontmatterMeta.data
4263

43-
if (frontmatter.excerpt) {
44-
const { html } = markdown.render(frontmatter.excerpt)
64+
if (this._frontmatterMeta.excerpt) {
65+
const { html } = markdown.render(this._frontmatterMeta.excerpt)
4566
this.excerpt = html
4667
}
4768

4869
// resolve i18n
4970
i18n.setSSRContext(this)
5071
this._i18n = i18n
5172

52-
this.permalink = getPermalink({
53-
pattern: this.frontmatter.permalink || permalinkPattern,
73+
this._permalink = getPermalink({
74+
pattern: this.frontmatter.permalink || this._permalinkPattern,
5475
slug: this.slug,
5576
date: this.frontmatter.date,
5677
localePath: i18n.$localePath,
5778
regularPath: this.regularPath
5879
})
5980

60-
if (this.permalink) {
61-
this.path = this.permalink
81+
if (this._permalink) {
82+
this.path = this._permalink
6283
}
6384
}
6485

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

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ module.exports = async function loadTheme (theme, sourceDir, vuepressDir) {
1313
(await fs.exists(localThemePath)) && ((await fs.readdir(localThemePath)).length > 0)
1414

1515
let themePath = null // Mandatory
16-
let themeLayoutPath = null // Mandatory
17-
let themeNotFoundPath = null // Optional
1816
let themeIndexFile = null // Optional
1917
let themePlugins = [] // Optional
2018
let themeName
@@ -40,33 +38,51 @@ module.exports = async function loadTheme (theme, sourceDir, vuepressDir) {
4038
try {
4139
themeIndexFile = require(themePath)
4240
} catch (error) {
43-
console.log(error)
4441
themeIndexFile = {}
4542
}
4643

4744
// handle theme api
48-
const { layout, notFound, plugins } = themeIndexFile
45+
const { plugins, layoutDir = useLocalTheme ? '.' : 'layouts' } = themeIndexFile
4946
themePlugins = plugins
50-
themeLayoutPath = layout
51-
? path.resolve(themePath, layout)
52-
: path.resolve(themePath, 'Layout.vue')
5347

54-
themeNotFoundPath = notFound
55-
? path.resolve(themePath, notFound)
56-
: path.resolve(themePath, 'NotFound.vue')
48+
const layoutDirPath = path.resolve(themePath, layoutDir)
5749

58-
if (!fs.existsSync(themeLayoutPath)) {
59-
throw new Error(`[vuepress] Cannot resolve Layout.vue file in \n ${themeLayoutPath}`)
50+
// normalize component name
51+
const getComponentName = filename => {
52+
filename = filename.slice(0, -4)
53+
if (filename === '404') {
54+
filename = 'NotFound'
55+
}
56+
return filename
57+
}
58+
59+
// built-in named layout or not.
60+
const isInternal = componentName => componentName === 'Layout' ||
61+
componentName === 'NotFound'
62+
63+
const layoutComponentMap = fs.readdirSync(layoutDirPath)
64+
.filter(filename => filename.endsWith('.vue'))
65+
.reduce((map, filename) => {
66+
const componentName = getComponentName(filename)
67+
const componentPath = path.resolve(layoutDirPath, filename)
68+
map[componentName] = { filename, componentName, path: componentPath }
69+
if (isInternal(componentName)) {
70+
map[componentName].isInternal = true
71+
}
72+
return map
73+
}, {})
74+
75+
if (!fs.existsSync(layoutComponentMap.Layout.path)) {
76+
throw new Error(`[vuepress] Cannot resolve Layout.vue file in \n ${layoutComponentMap.Layout.path}`)
6077
}
6178

62-
if (!fs.existsSync(themeNotFoundPath)) {
63-
themeNotFoundPath = path.resolve(__dirname, '../app/components/NotFound.vue')
79+
if (!fs.existsSync(layoutComponentMap.NotFound.path)) {
80+
layoutComponentMap['404'].path = path.resolve(__dirname, '../app/components/NotFound.vue')
6481
}
6582

6683
return {
6784
themePath,
68-
themeLayoutPath,
69-
themeNotFoundPath,
85+
layoutComponentMap,
7086
themeIndexFile,
7187
themePlugins,
7288
themeName,

0 commit comments

Comments
 (0)