mirror of
https://github.com/wahyd4/vuepress.git
synced 2026-08-09 05:16:23 +10:00
497 lines
13 KiB
JavaScript
Executable File
497 lines
13 KiB
JavaScript
Executable File
'use strict'
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
const createMarkdown = require('./createMarkdown')
|
|
const loadConfig = require('./loadConfig')
|
|
const loadTheme = require('./loadTheme')
|
|
const { getCacheLoaderOptions } = require('./CacheLoader')
|
|
const {
|
|
fs, path, logger, chalk, globby, sort,
|
|
datatypes: { isFunction },
|
|
fallback: { fsExistsFallback }
|
|
} = require('@vuepress/shared-utils')
|
|
|
|
const Page = require('./Page')
|
|
const ClientComputedMixin = require('./ClientComputedMixin')
|
|
const PluginAPI = require('./plugin-api')
|
|
const DevProcess = require('./dev')
|
|
const BuildProcess = require('./build')
|
|
const createTemp = require('./createTemp')
|
|
|
|
/**
|
|
* Expose VuePressApp.
|
|
*/
|
|
|
|
module.exports = class App {
|
|
/**
|
|
* Instantiate the app context with a new API
|
|
*
|
|
* @param {string} sourceDir
|
|
* @param {{
|
|
* plugins: pluginsConfig,
|
|
* theme: themeNameConfig
|
|
* temp: string
|
|
* }} options
|
|
*/
|
|
|
|
constructor (options = {}) {
|
|
this.options = options
|
|
this.sourceDir = this.options.sourceDir || path.join(__dirname, 'docs.fallback')
|
|
logger.debug('sourceDir', this.sourceDir)
|
|
if (!fs.existsSync(this.sourceDir)) {
|
|
logger.warn(`Source directory doesn't exist: ${chalk.yellow(this.sourceDir)}`)
|
|
}
|
|
|
|
const { tempPath, writeTemp } = createTemp(options.temp)
|
|
this.tempPath = tempPath
|
|
this.writeTemp = writeTemp
|
|
|
|
this.vuepressDir = path.resolve(this.sourceDir, '.vuepress')
|
|
this.libDir = path.join(__dirname, '../')
|
|
}
|
|
|
|
/**
|
|
* Resolve user config and initialize.
|
|
*
|
|
* @returns {void}
|
|
* @api private
|
|
*/
|
|
|
|
resolveConfigAndInitialize () {
|
|
if (this.options.siteConfig) {
|
|
this.siteConfig = this.options.siteConfig
|
|
} else {
|
|
let siteConfig = loadConfig(this.vuepressDir)
|
|
if (isFunction(siteConfig)) {
|
|
siteConfig = siteConfig(this)
|
|
}
|
|
this.siteConfig = siteConfig
|
|
}
|
|
|
|
// TODO custom cwd.
|
|
this.cwd = process.cwd()
|
|
|
|
this.base = this.siteConfig.base || '/'
|
|
this.themeConfig = this.siteConfig.themeConfig || {}
|
|
|
|
const rawOutDir = this.options.dest || this.siteConfig.dest
|
|
this.outDir = rawOutDir
|
|
? require('path').resolve(this.cwd, rawOutDir)
|
|
: require('path').resolve(this.sourceDir, '.vuepress/dist')
|
|
this.pages = [] // Array<Page>
|
|
this.pluginAPI = new PluginAPI(this)
|
|
this.ClientComputedMixinConstructor = ClientComputedMixin(this.getSiteData())
|
|
}
|
|
|
|
/**
|
|
* A asynchronous method used to prepare the context of the current app. which
|
|
* contains loading pages and plugins, apply plugins, etc.
|
|
*
|
|
* @returns {Promise<void>}
|
|
* @api private
|
|
*/
|
|
|
|
async process () {
|
|
this.resolveConfigAndInitialize()
|
|
this.normalizeHeadTagUrls()
|
|
this.themeAPI = loadTheme(this)
|
|
this.resolveTemplates()
|
|
this.resolveGlobalLayout()
|
|
|
|
this.applyInternalPlugins()
|
|
this.applyUserPlugins()
|
|
this.pluginAPI.initialize()
|
|
|
|
this.markdown = createMarkdown(this)
|
|
|
|
await this.resolvePages()
|
|
|
|
await this.pluginAPI.applyAsyncOption('additionalPages', this)
|
|
await Promise.all(
|
|
this.pluginAPI.getOption('additionalPages').appliedValues.map(async (options) => {
|
|
await this.addPage(options)
|
|
})
|
|
)
|
|
await this.pluginAPI.applyAsyncOption('ready')
|
|
await Promise.all([
|
|
this.pluginAPI.applyAsyncOption('clientDynamicModules', this),
|
|
this.pluginAPI.applyAsyncOption('enhanceAppFiles', this),
|
|
this.pluginAPI.applyAsyncOption('globalUIComponents', this)
|
|
])
|
|
}
|
|
|
|
/**
|
|
* Apply internal plugins
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
applyInternalPlugins () {
|
|
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/enhanceApp'))
|
|
.use(require('./internal-plugins/palette'))
|
|
.use(require('./internal-plugins/style'))
|
|
.use(require('./internal-plugins/layoutComponents'))
|
|
.use(require('./internal-plugins/pageComponents'))
|
|
.use(require('./internal-plugins/transformModule'))
|
|
.use(require('./internal-plugins/dataBlock'))
|
|
.use(require('./internal-plugins/frontmatterBlock'))
|
|
.use('container', {
|
|
type: 'slot',
|
|
before: info => `<template slot="${info}">`,
|
|
after: '</template>'
|
|
})
|
|
.use('container', {
|
|
type: 'v-pre',
|
|
before: '<div v-pre>',
|
|
after: '</div>'
|
|
})
|
|
.use('@vuepress/last-updated', !!shouldUseLastUpdated)
|
|
.use('@vuepress/register-components', {
|
|
componentsDir: [
|
|
path.resolve(this.sourceDir, '.vuepress/components'),
|
|
path.resolve(this.themeAPI.theme.path, 'global-components'),
|
|
this.themeAPI.existsParentTheme && path.resolve(this.themeAPI.parentTheme.path, 'global-components')
|
|
]
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Apply user plugins
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
applyUserPlugins () {
|
|
this.pluginAPI.useByPluginsConfig(this.options.plugins)
|
|
if (this.themeAPI.existsParentTheme) {
|
|
this.pluginAPI.use(this.themeAPI.parentTheme.entry)
|
|
}
|
|
this.pluginAPI
|
|
.use(this.themeAPI.theme.entry)
|
|
.use(this.themeAPI.vuepressPlugin)
|
|
.use(Object.assign({}, this.siteConfig, { name: '@vuepress/internal-site-config' }))
|
|
}
|
|
|
|
/**
|
|
* normalize head tag urls for base
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve options of cache loader.
|
|
*/
|
|
|
|
resolveCacheLoaderOptions () {
|
|
Object.assign(this, (getCacheLoaderOptions(this.siteConfig, this.options, this.cwd, this.isProd)))
|
|
}
|
|
|
|
/**
|
|
* Make template configurable
|
|
*
|
|
* Resolving Priority (devTemplate as example):
|
|
*
|
|
* 1. siteConfig.devTemplate
|
|
* 2. `dev.html` located at .vuepress/templates
|
|
* 3. themeEntryFile.devTemplate
|
|
* 4. default devTemplate
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
resolveTemplates () {
|
|
this.devTemplate = this.resolveCommonAgreementFilePath(
|
|
'devTemplate',
|
|
{
|
|
defaultValue: this.getLibFilePath('client/index.dev.html'),
|
|
siteAgreement: 'templates/dev.html',
|
|
themeAgreement: 'templates/dev.html'
|
|
}
|
|
)
|
|
|
|
this.ssrTemplate = this.resolveCommonAgreementFilePath(
|
|
'ssrTemplate',
|
|
{
|
|
defaultValue: this.getLibFilePath('client/index.ssr.html'),
|
|
siteAgreement: 'templates/ssr.html',
|
|
themeAgreement: 'templates/ssr.html'
|
|
}
|
|
)
|
|
|
|
logger.debug('SSR Template File: ' + chalk.gray(this.ssrTemplate))
|
|
logger.debug('DEV Template File: ' + chalk.gray(this.devTemplate))
|
|
}
|
|
|
|
/**
|
|
* resolve global layout
|
|
*
|
|
* @returns {string}
|
|
* @api private
|
|
*/
|
|
|
|
resolveGlobalLayout () {
|
|
this.globalLayout = this.resolveCommonAgreementFilePath(
|
|
'globalLayout',
|
|
{
|
|
defaultValue: this.getLibFilePath('client/components/GlobalLayout.vue'),
|
|
siteAgreement: `components/GlobalLayout.vue`,
|
|
themeAgreement: `layouts/GlobalLayout.vue`
|
|
}
|
|
)
|
|
|
|
logger.debug('globalLayout: ' + chalk.gray(this.globalLayout))
|
|
}
|
|
|
|
/**
|
|
* Resolve a path-type config.
|
|
*
|
|
* @param {string} configKey
|
|
* @param {string} defaultValue an absolute path
|
|
* @param {string} siteAgreement a relative path to vuepress dir
|
|
* @param {string} themeAgreement a relative path to theme dir
|
|
* @returns {string | void}
|
|
*/
|
|
|
|
resolveCommonAgreementFilePath (configKey, {
|
|
defaultValue,
|
|
siteAgreement,
|
|
themeAgreement
|
|
}) {
|
|
const siteConfigValue = this.siteConfig[configKey]
|
|
siteAgreement = this.resolveSiteAgreementFile(siteAgreement)
|
|
|
|
const themeConfigValue = this.getThemeConfigValue(configKey)
|
|
themeAgreement = this.resolveThemeAgreementFile(themeAgreement)
|
|
|
|
return fsExistsFallback([
|
|
siteConfigValue,
|
|
siteAgreement,
|
|
themeConfigValue,
|
|
themeAgreement,
|
|
defaultValue
|
|
].map(v => v))
|
|
}
|
|
|
|
/**
|
|
* Find all page source files located in sourceDir
|
|
*
|
|
* @returns {Promise<void>}
|
|
* @api private
|
|
*/
|
|
|
|
async resolvePages () {
|
|
// resolve pageFiles
|
|
const patterns = ['**/*.md', '**/*.vue', '!.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
|
|
*
|
|
* @returns {Promise<void>}
|
|
* @api public
|
|
*/
|
|
|
|
async addPage (options) {
|
|
options.permalinkPattern = this.siteConfig.permalink
|
|
const page = new Page(options, this)
|
|
await page.process({
|
|
markdown: this.markdown,
|
|
computed: new this.ClientComputedMixinConstructor(),
|
|
enhancers: this.pluginAPI.getOption('extendPageData').items
|
|
})
|
|
const index = this.pages.findIndex(({ path }) => path === page.path)
|
|
if (index >= 0) {
|
|
// Override a page if corresponding path already exists
|
|
logger.warn(`Override existing page ${chalk.yellow(page.path)}.`)
|
|
this.pages.splice(index, 1, page)
|
|
} else {
|
|
this.pages.push(page)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get config value of current active theme.
|
|
*
|
|
* @param {string} key
|
|
* @returns {any}
|
|
* @api private
|
|
*/
|
|
|
|
getThemeConfigValue (key) {
|
|
return this.themeAPI.theme.entry[key]
|
|
|| this.themeAPI.existsParentTheme && this.themeAPI.parentTheme.entry[key]
|
|
}
|
|
|
|
/**
|
|
* Resolve the absolute path of a theme-level agreement file,
|
|
* return `undefined` when it doesn't exists.
|
|
*
|
|
* @param {string} filepath
|
|
* @returns {string|undefined}
|
|
*/
|
|
|
|
resolveThemeAgreementFile (filepath) {
|
|
const current = path.resolve(this.themeAPI.theme.path, filepath)
|
|
if (fs.existsSync(current)) {
|
|
return current
|
|
}
|
|
if (this.themeAPI.existsParentTheme) {
|
|
const parent = path.resolve(this.themeAPI.parentTheme.path, filepath)
|
|
if (fs.existsSync(parent)) {
|
|
return parent
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve the absolute path of a site-level agreement file,
|
|
* return `undefined` when it doesn't exists.
|
|
*
|
|
* @param {string} filepath
|
|
* @returns {string|undefined}
|
|
*/
|
|
|
|
resolveSiteAgreementFile (filepath) {
|
|
return path.resolve(this.vuepressDir, filepath)
|
|
}
|
|
|
|
/**
|
|
* Get the data to be delivered to the client.
|
|
*
|
|
* @returns {{
|
|
* title: string,
|
|
* description: string,
|
|
* base: string,
|
|
* pages: Page[],
|
|
* themeConfig: ThemeConfig,
|
|
* locales: Locales
|
|
* }}
|
|
* @api public
|
|
*/
|
|
|
|
getSiteData () {
|
|
const { locales } = this.siteConfig
|
|
if (locales) {
|
|
Object.keys(locales).forEach(path => {
|
|
locales[path].path = path
|
|
})
|
|
}
|
|
|
|
return {
|
|
title: this.siteConfig.title || '',
|
|
description: this.siteConfig.description || '',
|
|
base: this.base,
|
|
pages: this.pages.map(page => page.toJson()),
|
|
themeConfig: this.siteConfig.themeConfig || {},
|
|
locales
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get file path in core lib
|
|
*
|
|
* @param relative
|
|
* @returns {string}
|
|
* @api public
|
|
*/
|
|
|
|
getLibFilePath (relative) {
|
|
return path.join(this.libDir, relative)
|
|
}
|
|
|
|
/**
|
|
* Launch a dev process with current app context.
|
|
*
|
|
* @returns {Promise<App>}
|
|
* @api public
|
|
*/
|
|
|
|
async dev () {
|
|
this.isProd = false
|
|
this.devProcess = new DevProcess(this)
|
|
await this.devProcess.process()
|
|
const error = await new Promise(resolve => {
|
|
try {
|
|
this.devProcess
|
|
.on('fileChanged', ({ type, target }) => {
|
|
console.log(`Reload due to ${chalk.red(type)} ${chalk.cyan(path.relative(this.sourceDir, target))}`)
|
|
this.process()
|
|
})
|
|
.createServer()
|
|
.listen(resolve)
|
|
} catch (err) {
|
|
resolve(err)
|
|
}
|
|
})
|
|
if (error) {
|
|
throw error
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Launch a build process with current app context
|
|
*
|
|
* @returns {Promise<App>}
|
|
* @api public
|
|
*/
|
|
|
|
async build () {
|
|
this.isProd = true
|
|
this.buildProcess = new BuildProcess(this)
|
|
await this.buildProcess.process()
|
|
await this.buildProcess.render()
|
|
return this
|
|
}
|
|
}
|
|
|