refactor: refine core implementation (#762)

This commit is contained in:
ULIVZ
2018-08-24 00:02:33 +08:00
committed by GitHub
parent 45252a9fd4
commit afa71b582b
16 changed files with 361 additions and 358 deletions
+2 -2
View File
@@ -65,12 +65,12 @@ module.exports = async function build (sourceDir, cliOptions = {}) {
// render pages
logger.wait('Rendering static HTML...')
for (const page of options.siteData.pages) {
for (const page of options.pages) {
await renderPage(page)
}
// if the user does not have a custom 404.md, generate the theme's default
if (!options.siteData.pages.some(p => p.path === '/404.html')) {
if (!options.pages.some(p => p.path === '/404.html')) {
await renderPage({ path: '/404.html' })
}
+2 -2
View File
@@ -81,7 +81,7 @@ module.exports = async function dev (sourceDir, cliOptions = {}) {
.use(DevLogPlugin, [{
port,
displayHost,
publicPath: options.publicPath
publicPath: options.base
}])
config = config.toConfig()
@@ -118,7 +118,7 @@ module.exports = async function dev (sourceDir, cliOptions = {}) {
// respect base when serving static files...
if (fs.existsSync(userPublic)) {
app.use(mount(options.publicPath, serveStatic(userPublic)))
app.use(mount(options.base, serveStatic(userPublic)))
}
app.use(convert(history({
@@ -1,7 +1,7 @@
function genImportAsyncComponentFile (pages) {
return `export function loadComponent (key) {
switch (key) {
${pages.map(({ key, filePath }) => ` case "${key}": return import("${filePath}");`).join('\n')}
${pages.map(({ key, _filePath }) => ` case "${key}": return import("${_filePath}");`).join('\n')}
}
}`
}
@@ -11,7 +11,7 @@ module.exports = (options, context) => ({
// @internal/async-component
async clientDynamicModules () {
const importAsyncComponentCode = genImportAsyncComponentFile(context.siteData.pages)
const importAsyncComponentCode = genImportAsyncComponentFile(context.pages)
return {
name: 'async-component.js',
content: importAsyncComponentCode,
@@ -3,7 +3,7 @@ module.exports = (options, context) => ({
// @internal/routes
async clientDynamicModules () {
const routesCode = await genRoutesFile(context.siteData.pages)
const routesCode = await genRoutesFile(context.pages)
return { name: 'routes.js', content: routesCode, dirname: 'internal' }
}
})
@@ -14,11 +14,15 @@ module.exports = (options, context) => ({
* @returns {Promise<string>}
*/
async function genRoutesFile (pages) {
function genRoute ({ path: pagePath, filePath, key: componentName }) {
function genRoute ({
path: pagePath,
key: componentName,
frontmatter = {}
}) {
let code = `
{
name: ${JSON.stringify(componentName)},
path: ${JSON.stringify(pagePath)},
path: ${JSON.stringify(frontmatter.permalink || pagePath)},
component: ThemeLayout,
beforeEnter: (to, from, next) => {
registerComponent(${JSON.stringify(componentName)}).then(() => next())
@@ -3,7 +3,7 @@ module.exports = (options, context) => ({
// @internal/siteData
async clientDynamicModules () {
const code = `export const siteData = ${JSON.stringify(context.siteData, null, 2)}`
const code = `export const siteData = ${JSON.stringify(context.getSiteData(), null, 2)}`
return { name: 'siteData.js', content: code, dirname: 'internal' }
}
})
@@ -1,64 +0,0 @@
const path = require('path')
const { writeTemp } = require('@vuepress/shared-utils')
class PluginContext {
constructor (options) {
Object.defineProperty(this, '_options', {
enumerable: false,
configurable: false,
writable: false,
value: options
})
}
get base () {
return this._options.siteConfig.base
}
get isProd () {
return this._options.isProd
}
get sourceDir () {
return this._options.sourceDir
}
get publicDir () {
path.resolve(this.sourceDir, '.vuepress/public')
}
get outDir () {
return this._options.outDir
}
get themePath () {
return this._options.themePath
}
get publicPath () {
return this._options.publicPath
}
get themeConfig () {
return this._options.themeConfig
}
get siteConfig () {
return this._options.siteConfig
}
get siteData () {
return this._options.siteData
}
get self () {
return this._options.self
}
}
Object.assign(PluginContext.prototype, {
resolve: path.resolve,
writeTemp
})
module.exports = PluginContext
@@ -107,11 +107,14 @@ exports.hydratePlugin = function ({ config, name, shortcut, isLocal }, pluginOpt
exports.normalizePluginsConfig = function (pluginsConfig) {
const { valid, warnMsg } = assertTypes(pluginsConfig, [Object, Array])
if (!valid) {
logger.warn(
`[${chalk.gray('config')}] ` +
`Invalid value for "plugin" field : ${warnMsg}`
)
if (pluginsConfig !== undefined) {
logger.warn(
`[${chalk.gray('config')}] ` +
`Invalid value for "plugin" field : ${warnMsg}`
)
}
pluginsConfig = []
return pluginsConfig
}
if (Array.isArray(pluginsConfig)) {
pluginsConfig = pluginsConfig.map(item => {
@@ -0,0 +1,249 @@
const path = require('path')
const createMarkdown = require('../markdown/index')
const loadConfig = require('./loadConfig')
const { globby } = require('@vuepress/shared-utils')
const { sort } = require('./util')
const { fs, logger, chalk } = require('@vuepress/shared-utils')
const Page = require('./Page')
const PluginAPI = require('../plugin-api/index')
module.exports = class AppContext {
/**
* Instantiate the app context with a new API
* @param { string } sourceDir
* @param {{
* isProd: boolean,
* plugins: pluginsConfig,
* theme: themeNameConfig
* }} options
*/
constructor (sourceDir, options) {
this.sourceDir = sourceDir
this._options = options
this.isProd = options.isProd
this.vuepressDir = path.resolve(sourceDir, '.vuepress')
this.siteConfig = loadConfig(this.vuepressDir)
this.base = this.siteConfig.base || '/'
this.themeConfig = this.siteConfig.themeConfig || {}
this.outDir = this.siteConfig.dest
? path.resolve(this.siteConfig.dest)
: path.resolve(sourceDir, '.vuepress/dist')
this.markdown = createMarkdown(this.siteConfig)
this.pluginAPI = new PluginAPI(this)
this.pages = [] // Array<Page>
}
/**
* Load pages, load plugins, apply plugins / plugin options, etc.
* @returns {Promise<void>}
*/
async process () {
this.normalizeHeadTagUrls()
await this.resolveTheme()
this.resolvePlugins()
await this.resolvePages()
await this.pluginAPI.options.additionalPages.values.map(async ({ path, permalink }) => {
await this.addPage(path, { permalink })
})
await this.pluginAPI.options.ready.apply()
this.pluginAPI.options.extendMarkdown.syncApply(this.markdown)
await this.pluginAPI.options.clientDynamicModules.apply()
await this.pluginAPI.options.globalUIComponents.apply()
await this.pluginAPI.options.enhanceAppFiles.apply()
}
/**
* Apply internal and user plugins
*/
resolvePlugins () {
const themeConfig = this.themeConfig
const siteConfig = this.siteConfig
const shouldUseLastUpdated = (
themeConfig.lastUpdated ||
Object.keys(siteConfig.locales && themeConfig.locales || {})
.some(base => themeConfig.locales[base].lastUpdated)
)
this.pluginAPI
// internl core plugins
.use(require('../internal-plugins/siteData'))
.use(require('../internal-plugins/routes'))
.use(require('../internal-plugins/rootMixins'))
.use(require('../internal-plugins/importAsyncComponent'))
.use(require('../internal-plugins/enhanceApp'))
.use(require('../internal-plugins/overrideCSS'))
.use(require('../internal-plugins/data-mixins'))
// user plugin
.useByPluginsConfig(this._options.plugins)
.useByPluginsConfig(this.siteConfig.plugins)
.useByPluginsConfig(this.themeplugins)
// built-in plugins
.use('@vuepress/last-updated', shouldUseLastUpdated)
.use('@vuepress/register-components', {
componentsDir: [
path.resolve(this.sourceDir, '.vuepress/components'),
path.resolve(this.themePath, 'components')
]
})
.apply()
}
/**
* normalize head tag urls for base
*/
normalizeHeadTagUrls () {
if (this.base !== '/' && this.siteConfig.head) {
this.siteConfig.head.forEach(tag => {
const attrs = tag[1]
if (attrs) {
for (const name in attrs) {
if (name === 'src' || name === 'href') {
const value = attrs[name]
if (value.charAt(0) === '/') {
attrs[name] = this.base + value.slice(1)
}
}
}
}
})
}
}
/**
* Find all page source files located in sourceDir
* @returns {Promise<void>}
*/
async resolvePages () {
// resolve pageFiles
const patterns = ['**/*.md', '!.vuepress', '!node_modules']
if (this.siteConfig.dest) {
// #654 exclude dest folder when dest dir was set in
// sourceDir but not in '.vuepress'
const outDirRelative = path.relative(this.sourceDir, this.outDir)
if (!outDirRelative.includes('..')) {
patterns.push('!' + outDirRelative)
}
}
const pageFiles = sort(await globby(patterns, { cwd: this.sourceDir }))
await Promise.all(pageFiles.map(async (relative) => {
const filePath = path.resolve(this.sourceDir, relative)
await this.addPage(filePath, { relative })
}))
}
/**
* Add a page
* @param { string } filePath
* @param { string } relative relative path of source markdown file.
* @param { string } permalink the URL (excluding the domain name)
* for your pages, posts.
* @returns { Promise<void> }
*/
async addPage (filePath, { relative, permalink }) {
const page = new Page(filePath, { relative, permalink })
await page.process(this.markdown)
await this.pluginAPI.options.extendPageData.apply(page)
this.pages.push(page)
}
/**
* Resolve theme
* @returns { Promise<void> }
*/
async resolveTheme () {
const theme = this.siteConfig.theme || this._options.theme
const requireResolve = (target) => {
return require.resolve(target, {
paths: [
path.resolve(__dirname, '../../node_modules'),
path.resolve(this.sourceDir)
]
})
}
// resolve theme
const localThemePath = path.resolve(this.vuepressDir, 'theme')
const useLocalTheme = await fs.exists(localThemePath)
let themePath = null
let themeLayoutPath = null
let themeNotFoundPath = null
let themeIndexFile = null
let themePlugins = []
if (useLocalTheme) {
logger.tip(`\nApply theme located at ${localThemePath}...`)
// use local custom theme
themePath = localThemePath
themeLayoutPath = path.resolve(localThemePath, 'Layout.vue')
themeNotFoundPath = path.resolve(localThemePath, 'NotFound.vue')
if (!fs.existsSync(themeLayoutPath)) {
throw new Error(`[vuepress] Cannot resolve Layout.vue file in .vuepress/theme.`)
}
if (!fs.existsSync(themeNotFoundPath)) {
throw new Error(`[vuepress] Cannot resolve NotFound.vue file in .vuepress/theme.`)
}
} else if (theme) {
// use external theme
try {
// backward-compatible 0.x.x.
themeLayoutPath = requireResolve(`vuepress-theme-${theme}/Layout.vue`)
themePath = path.dirname(themeLayoutPath)
themeNotFoundPath = path.resolve(themeLayoutPath, 'NotFound.vue')
} catch (e) {
try {
themeIndexFile = requireResolve(`vuepress-theme-${theme}/index.js`)
} catch (e) {
try {
themeIndexFile = requireResolve(`@vuepress/theme-${theme}`)
themePath = path.dirname(themeIndexFile)
themeIndexFile = require(themeIndexFile)
themeLayoutPath = themeIndexFile.layout
themeNotFoundPath = themeIndexFile.notFound
themePlugins = themeIndexFile.plugins
} catch (e) {
throw new Error(`[vuepress] Failed to load custom theme "${theme}". File vuepress-theme-${theme}/Layout.vue does not exist.`)
}
}
}
logger.tip(`\nApply theme ${chalk.gray(theme)}`)
} else {
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`)
}
this.themePath = themePath
this.themeLayoutPath = themeLayoutPath
this.themeNotFoundPath = themeNotFoundPath
this.themeplugins = themePlugins
}
/**
* Get the data to be delivered to the client.
* @returns {{
* title: string,
* description: string,
* base: string,
* pages: Page[],
* themeConfig: ThemeConfig,
* locales: Locales
* }}
*/
getSiteData () {
return {
title: this.siteConfig.title || '',
description: this.siteConfig.description || '',
base: this.base,
pages: this.pages.map(page => page.toJson()),
themeConfig: this.siteConfig.themeConfig || {},
locales: this.siteConfig.locales
}
}
}
@@ -0,0 +1,72 @@
const path = require('path')
const slugify = require('../markdown/slugify')
const { fs } = require('@vuepress/shared-utils')
const { encodePath, fileToPath } = require('./util')
const {
inferTitle,
extractHeaders,
parseFrontmatter
} = require('../util/index')
module.exports = class Page {
constructor (filePath, {
relative,
routePath
}) {
this._filePath = filePath
if (relative) {
this._routePath = encodePath(fileToPath(relative))
} else {
this._routePath = routePath
}
this.path = this._routePath
}
async process (markdown) {
this.key = 'v-' + Math.random().toString(16).slice(2)
this.content = await fs.readFile(this._filePath, 'utf-8')
const frontmatter = parseFrontmatter(this.content)
// infer title
const title = inferTitle(frontmatter)
if (title) {
this.title = title
}
// headers
const headers = extractHeaders(
frontmatter.content,
['h2', 'h3'],
markdown
)
if (headers.length) {
this.headers = headers
}
this.frontmatter = frontmatter.data
if (frontmatter.excerpt) {
const { html } = markdown.render(frontmatter.excerpt)
this.excerpt = html
}
}
get filename () {
return path.parse(this._filePath).name
}
get slug () {
return slugify(this.filename)
}
toJson () {
const json = {}
Object.keys(this).reduce((json, key) => {
if (!key.startsWith('_')) {
json[key] = this[key]
}
return json
}, json)
return json
}
}
+8 -60
View File
@@ -1,66 +1,14 @@
const path = require('path')
const resolveOptions = require('./resolveOptions')
const resolveSiteData = require('./resolveSiteData')
const PluginAPI = require('../plugin-api/index')
const PluginContext = require('../plugin-api/context')
const AppContext = require('./AppContext')
module.exports = async function prepare ({
sourceDir,
isProd,
cliOptions
cliOptions: {
plugins,
theme
}
}) {
// 1. load options
const options = await resolveOptions(sourceDir, cliOptions)
options.isProd = isProd
// 2. apply plugins
const { siteConfig, themeConfig, themePath, themePlugins, cliPlugins, markdown } = options
const pluginContext = new PluginContext(options)
const pluginAPI = new PluginAPI(pluginContext)
options.pluginAPI = pluginAPI
const shouldUseLastUpdated = (
themeConfig.lastUpdated ||
Object.keys(siteConfig.locales && themeConfig.locales || {})
.some(base => themeConfig.locales[base].lastUpdated)
)
pluginAPI
// internl core plugins
.use(require('../internal-plugins/routes'))
.use(require('../internal-plugins/rootMixins'))
.use(require('../internal-plugins/importAsyncComponent'))
.use(require('../internal-plugins/enhanceApp'))
.use(require('../internal-plugins/siteData'))
.use(require('../internal-plugins/overrideCSS'))
.use(require('../internal-plugins/data-mixins'))
// user plugin
.useByPluginsConfig(cliPlugins)
.useByPluginsConfig(siteConfig.plugins)
.useByPluginsConfig(themePlugins)
// built-in plugins
.use('@vuepress/last-updated', shouldUseLastUpdated)
.use('@vuepress/register-components', {
componentsDir: [
path.resolve(sourceDir, '.vuepress/components'),
path.resolve(themePath, 'components')
]
})
.apply()
// 3. resolve siteData
// SiteData must be resolved after the plugin initialization
// because plugins can be able to extend the sitedata.
options.siteData = await resolveSiteData(options)
// 4. ready hook, user can do some options transformation here.
await pluginAPI.options.ready.apply()
// 5. apply plugin options
pluginAPI.options.extendMarkdown.syncApply(markdown)
await pluginAPI.options.clientDynamicModules.apply()
await pluginAPI.options.globalUIComponents.apply()
await pluginAPI.options.enhanceAppFiles.apply()
return options
const appContext = new AppContext(sourceDir, { plugins, theme, isProd })
await appContext.process()
return appContext
}
@@ -1,129 +0,0 @@
const path = require('path')
const createMarkdown = require('../markdown/index')
const loadConfig = require('./loadConfig')
const { sort } = require('./util')
const { chalk, fs, logger, globby } = require('@vuepress/shared-utils')
module.exports = async function resolveOptions (sourceDir, cliOptions) {
function requireResolve (target) {
return require.resolve(target, {
paths: [
path.resolve(__dirname, '../../node_modules'),
path.resolve(sourceDir)
]
})
}
const vuepressDir = path.resolve(sourceDir, '.vuepress')
const siteConfig = loadConfig(vuepressDir)
// normalize head tag urls for base
const base = siteConfig.base || '/'
if (base !== '/' && siteConfig.head) {
siteConfig.head.forEach(tag => {
const attrs = tag[1]
if (attrs) {
for (const name in attrs) {
if (name === 'src' || name === 'href') {
const value = attrs[name]
if (value.charAt(0) === '/') {
attrs[name] = base + value.slice(1)
}
}
}
}
})
}
// resolve outDir
const outDir = siteConfig.dest
? path.resolve(siteConfig.dest)
: path.resolve(sourceDir, '.vuepress/dist')
// resolve theme
const localThemePath = path.resolve(vuepressDir, 'theme')
const useLocalTheme = fs.existsSync(localThemePath)
const theme = siteConfig.theme || cliOptions.theme
let themePath = null
let themeLayoutPath = null
let themeNotFoundPath = null
let themeIndexFile = null
let themePlugins = []
if (useLocalTheme) {
logger.tip(`\nApply theme located at ${localThemePath}...`)
// use local custom theme
themePath = localThemePath
themeLayoutPath = path.resolve(localThemePath, 'Layout.vue')
themeNotFoundPath = path.resolve(localThemePath, 'NotFound.vue')
if (!fs.existsSync(themeLayoutPath)) {
throw new Error(`[vuepress] Cannot resolve Layout.vue file in .vuepress/theme.`)
}
if (!fs.existsSync(themeNotFoundPath)) {
throw new Error(`[vuepress] Cannot resolve NotFound.vue file in .vuepress/theme.`)
}
} else if (theme) {
// use external theme
try {
// backward-compatible 0.x.x.
themeLayoutPath = requireResolve(`vuepress-theme-${theme}/Layout.vue`)
themePath = path.dirname(themeLayoutPath)
themeNotFoundPath = path.resolve(themeLayoutPath, 'NotFound.vue')
} catch (e) {
try {
themeIndexFile = requireResolve(`vuepress-theme-${theme}/index.js`)
} catch (e) {
try {
themeIndexFile = requireResolve(`@vuepress/theme-${theme}`)
themePath = path.dirname(themeIndexFile)
themeIndexFile = require(themeIndexFile)
themeLayoutPath = themeIndexFile.layout
themeNotFoundPath = themeIndexFile.notFound
themePlugins = themeIndexFile.plugins
} catch (e) {
throw new Error(`[vuepress] Failed to load custom theme "${theme}". File vuepress-theme-${theme}/Layout.vue does not exist.`)
}
}
}
logger.tip(`\nApply theme ${chalk.gray(theme)}`)
} else {
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`)
}
// resolve theme config
const themeConfig = siteConfig.themeConfig || {}
// resolve markdown
const markdown = createMarkdown(siteConfig)
// resolve pageFiles
const patterns = ['**/*.md', '!.vuepress', '!node_modules']
if (siteConfig.dest) {
// #654 exclude dest folder when dest dir was set in
// sourceDir but not in '.vuepress'
const outDirRelative = path.relative(sourceDir, outDir)
if (!outDirRelative.includes('..')) {
patterns.push('!' + outDirRelative)
}
}
const pageFiles = sort(await globby(patterns, { cwd: sourceDir }))
const options = {
siteConfig,
themeConfig,
sourceDir,
outDir,
publicPath: base,
pageFiles,
themePath,
themeLayoutPath,
themeNotFoundPath,
themePlugins,
cliPlugins: cliOptions.plugins || [],
markdown
}
return options
}
@@ -1,82 +0,0 @@
const { fs } = require('@vuepress/shared-utils')
const path = require('path')
const { encodePath, fileToPath } = require('./util')
const {
inferTitle,
extractHeaders,
parseFrontmatter
} = require('../util/index')
module.exports = async function ({
sourceDir,
pageFiles,
pluginAPI,
markdown,
siteConfig,
themeConfig,
publicPath
}) {
async function getPageData ({ filePath, routePath, base }) {
const key = 'v-' + Math.random().toString(16).slice(2)
const data = { key, path: routePath, filePath }
const content = await fs.readFile(filePath, 'utf-8')
// extract yaml frontmatter
const frontmatter = parseFrontmatter(content)
// infer title
const title = inferTitle(frontmatter)
if (title) {
data.title = title
}
const headers = extractHeaders(
frontmatter.content,
['h2', 'h3'],
markdown
)
if (headers.length) {
data.headers = headers
}
data.frontmatter = frontmatter.data
if (frontmatter.excerpt) {
const { html } = markdown.render(frontmatter.excerpt)
data.excerpt = html
}
await pluginAPI.options.extendPageData.apply(data)
return data
}
// resolve pagesData
const pagesData = await Promise.all(pageFiles.map(async (base) => {
const filePath = path.resolve(sourceDir, base)
const routePath = encodePath(fileToPath(base))
return getPageData({ filePath, routePath, base })
}))
// resolve additional pagesData
const additionalPagesData = await Promise.all(
pluginAPI.options.additionalPages.values.map(async ({ route: routePath, path: filePath }) => {
if (!fs.existsSync(filePath)) {
throw new Error(`[vuepress] Cannot resolve additional page: ${filePath}`)
}
return getPageData({ filePath, routePath })
})
)
const siteData = {
title: siteConfig.title || '',
description: siteConfig.description || '',
base: publicPath,
pages: [
...pagesData,
...additionalPagesData
],
themeConfig,
locales: siteConfig.locales
}
return siteData
}
@@ -2,14 +2,12 @@ const path = require('path')
module.exports = function createBaseConfig ({
siteConfig,
siteData,
sourceDir,
outDir,
publicPath,
base: publicPath,
themePath,
themeLayoutPath,
themeNotFoundPath,
isAlgoliaSearch,
markdown
}, { debug } = {}, isServer) {
const Config = require('webpack-chain')
@@ -1,6 +1,8 @@
const path = require('path')
module.exports = (options, context) => ({
enhanceAppFiles: [
context.resolve(__dirname, 'client.js')
path.resolve(__dirname, 'client.js')
],
globalUIComponents: 'BackToTop'
+5 -3
View File
@@ -1,3 +1,5 @@
const path = require('path')
module.exports = (pluginOptions = {}, context) => ({
name: 'i18n-ui',
@@ -5,14 +7,14 @@ module.exports = (pluginOptions = {}, context) => ({
enabled: !context.isProd,
enhanceAppFiles: [
context.resolve(__dirname, 'client.js')
path.resolve(__dirname, 'client.js')
],
additionalPages: [
{
override: false,
route: pluginOptions.route || '/i18n/',
path: context.resolve(__dirname, 'index.md')
permalink: pluginOptions.route || '/i18n/',
path: path.resolve(__dirname, 'index.md')
}
]
})
@@ -1,9 +1,9 @@
const spawn = require('cross-spawn')
module.exports = (options = {}, context) => ({
extendPageData ({ filePath }) {
extendPageData ({ _filePath }) {
const { transformer } = options
const timestamp = getGitLastUpdatedTimeStamp(filePath)
const timestamp = getGitLastUpdatedTimeStamp(_filePath)
const lastUpdated = typeof transformer === 'function' ? transformer(timestamp) : timestamp
return { lastUpdated }
}