feat: refine node api (#1395)

- fix($core): markdown slot doesn‘t work. (regression of c85f62d)
- docs: fresh Node.JS API.
- test: official plugins.
This commit is contained in:
ULIVZ
2019-03-04 23:46:13 +08:00
committed by GitHub
parent 7148df2e82
commit e5d8ed4655
180 changed files with 3887 additions and 3743 deletions
+3
View File
@@ -3,6 +3,9 @@ module.exports = {
'test': {
'presets': [
['@babel/preset-env', { 'targets': { 'node': 'current' }}]
],
'plugins': [
'@babel/plugin-syntax-dynamic-import'
]
}
}
+1
View File
@@ -13,6 +13,7 @@
"remote-version": "node scripts/remote-version.js",
"dev": "yarn tsc && yarn workspace docs dev",
"build": "yarn tsc && yarn workspace docs build",
"view-info": "yarn tsc && yarn workspace docs view-info",
"show-help": "yarn workspace docs show-help",
"dev:blog": "yarn tsc && yarn workspace blog dev",
"build:blog": "yarn tsc && yarn workspace blog build",
-197
View File
@@ -1,197 +0,0 @@
'use strict'
module.exports = async function build (sourceDir, cliOptions = {}) {
process.env.NODE_ENV = 'production'
const webpack = require('webpack')
const readline = require('readline')
const escape = require('escape-html')
const { chalk, fs, path, logger, env, performance } = require('@vuepress/shared-utils')
const prepare = require('./prepare/index')
const createClientConfig = require('./webpack/createClientConfig')
const createServerConfig = require('./webpack/createServerConfig')
const { createBundleRenderer } = require('vue-server-renderer')
const { normalizeHeadTag, applyUserWebpackConfig } = require('./util/index')
logger.wait('Extracting site metadata...')
const ctx = await prepare(sourceDir, cliOptions, true /* isProd */)
const { outDir, cwd } = ctx
if (cwd === outDir) {
return console.error(logger.error(chalk.red('Unexpected option: outDir cannot be set to the current working directory.\n'), false))
}
await fs.emptyDir(outDir)
logger.debug('Dist directory: ' + chalk.gray(outDir))
let clientConfig = createClientConfig(ctx, cliOptions).toConfig()
let serverConfig = createServerConfig(ctx, cliOptions).toConfig()
// apply user config...
const userConfig = ctx.siteConfig.configureWebpack
if (userConfig) {
clientConfig = applyUserWebpackConfig(userConfig, clientConfig, false)
serverConfig = applyUserWebpackConfig(userConfig, serverConfig, true)
}
// compile!
const stats = await compile([clientConfig, serverConfig])
const serverBundle = require(path.resolve(outDir, 'manifest/server.json'))
const clientManifest = require(path.resolve(outDir, 'manifest/client.json'))
// remove manifests after loading them.
await fs.remove(path.resolve(outDir, 'manifest'))
// find and remove empty style chunk caused by
// https://github.com/webpack-contrib/mini-css-extract-plugin/issues/85
// TODO remove when it's fixed
if (!clientConfig.devtool && (!clientConfig.plugins ||
!clientConfig.plugins.some(p =>
p instanceof webpack.SourceMapDevToolPlugin ||
p instanceof webpack.EvalSourceMapDevToolPlugin
))) {
await workaroundEmptyStyleChunk()
}
// create server renderer using built manifests
const renderer = createBundleRenderer(serverBundle, {
clientManifest,
runInNewContext: false,
inject: false,
shouldPrefetch: ctx.siteConfig.shouldPrefetch || (() => true),
template: await fs.readFile(ctx.ssrTemplate, 'utf-8')
})
// pre-render head tags from user config
const userHeadTags = (ctx.siteConfig.head || [])
.map(renderHeadTag)
.join('\n ')
// if the user does not have a custom 404.md, generate the theme's default
if (!ctx.pages.some(p => p.path === '/404.html')) {
ctx.addPage({ path: '/404.html' })
}
// render pages
logger.wait('Rendering static HTML...')
const pagePaths = []
for (const page of ctx.pages) {
pagePaths.push(await renderPage(page))
}
readline.clearLine(process.stdout, 0)
readline.cursorTo(process.stdout, 0)
await ctx.pluginAPI.options.generated.apply(pagePaths)
// DONE.
const relativeDir = path.relative(cwd, outDir)
logger.success(`Generated static files in ${chalk.cyan(relativeDir)}.`)
const { duration } = performance.stop()
logger.developer(`It took a total of ${chalk.cyan(`${duration}ms`)} to run the ${chalk.cyan('vuepress build')}.`)
console.log()
// --- helpers ---
function compile (config) {
return new Promise((resolve, reject) => {
webpack(config, (err, stats) => {
if (err) {
return reject(err)
}
if (stats.hasErrors()) {
stats.toJson().errors.forEach(err => {
console.error(err)
})
reject(new Error(`Failed to compile with errors.`))
return
}
if (env.isDebug && stats.hasWarnings()) {
stats.toJson().warnings.forEach(warning => {
console.warn(warning)
})
}
resolve(stats.toJson({ modules: false }))
})
})
}
function renderHeadTag (tag) {
const { tagName, attributes, innerHTML, closeTag } = normalizeHeadTag(tag)
return `<${tagName}${renderAttrs(attributes)}>${innerHTML}${closeTag ? `</${tagName}>` : ``}`
}
function renderAttrs (attrs = {}) {
const keys = Object.keys(attrs)
if (keys.length) {
return ' ' + keys.map(name => `${name}="${escape(attrs[name])}"`).join(' ')
} else {
return ''
}
}
async function renderPage (page) {
const pagePath = page.path
readline.clearLine(process.stdout, 0)
readline.cursorTo(process.stdout, 0)
process.stdout.write(`Rendering page: ${pagePath}`)
// #565 Avoid duplicate description meta at SSR.
const meta = (page.frontmatter && page.frontmatter.meta || []).filter(item => item.name !== 'description')
const pageMeta = renderPageMeta(meta)
const context = {
url: pagePath,
userHeadTags,
pageMeta,
title: 'VuePress',
lang: 'en',
description: ''
}
let html
try {
html = await renderer.renderToString(context)
} catch (e) {
console.error(logger.error(chalk.red(`Error rendering ${pagePath}:`), false))
throw e
}
const filename = decodeURIComponent(pagePath.replace(/\/$/, '/index.html').replace(/^\//, ''))
const filePath = path.resolve(outDir, filename)
await fs.ensureDir(path.dirname(filePath))
await fs.writeFile(filePath, html)
return filePath
}
function renderPageMeta (meta) {
if (!meta) return ''
return meta.map(m => {
let res = `<meta`
Object.keys(m).forEach(key => {
res += ` ${key}="${escape(m[key])}"`
})
return res + `>`
}).join('')
}
async function workaroundEmptyStyleChunk () {
const styleChunk = stats.children[0].assets.find(a => {
return /styles\.\w{8}\.js$/.test(a.name)
})
if (!styleChunk) return
const styleChunkPath = path.resolve(outDir, styleChunk.name)
const styleChunkContent = await fs.readFile(styleChunkPath, 'utf-8')
await fs.remove(styleChunkPath)
// prepend it to app.js.
// this is necessary for the webpack runtime to work properly.
const appChunk = stats.children[0].assets.find(a => {
return /app\.\w{8}\.js$/.test(a.name)
})
const appChunkPath = path.resolve(outDir, appChunk.name)
const appChunkContent = await fs.readFile(appChunkPath, 'utf-8')
await fs.writeFile(appChunkPath, styleChunkContent + appChunkContent)
}
}
@@ -1,8 +1,8 @@
/* global VUEPRESS_TEMP_PATH */
import Vue from 'vue'
import GLobalVue from 'vue'
export default function dataMixin (I18n, siteData) {
export default function dataMixin (I18n, siteData, Vue = GLobalVue) {
prepare(siteData)
Vue.$vuepress.$set('siteData', siteData)
-207
View File
@@ -1,207 +0,0 @@
'use strict'
module.exports = async (sourceDir, cliOptions = {}, ctx) => {
const { server, host, port } = await prepareServer(sourceDir, cliOptions, ctx)
server.listen(port, host, err => {
if (err) {
console.log(err)
}
})
}
module.exports.prepare = prepareServer
async function prepareServer (sourceDir, cliOptions = {}, context) {
const WebpackDevServer = require('webpack-dev-server')
const { path } = require('@vuepress/shared-utils')
const webpack = require('webpack')
const chokidar = require('chokidar')
const prepare = require('./prepare/index')
const { chalk, fs, logger } = require('@vuepress/shared-utils')
const HeadPlugin = require('./webpack/HeadPlugin')
const DevLogPlugin = require('./webpack/DevLogPlugin')
const createClientConfig = require('./webpack/createClientConfig')
const { applyUserWebpackConfig } = require('./util/index')
const { frontmatterEmitter } = require('@vuepress/markdown-loader')
const ctx = context || await prepare(sourceDir, cliOptions, false /* isProd */)
// setup watchers to update options and dynamically generated files
const update = (reason) => {
console.log(`Reload due to ${reason}`)
ctx.pluginAPI.options.updated.syncApply()
prepare(sourceDir, cliOptions, false /* isProd */).catch(err => {
console.error(logger.error(chalk.red(err.stack), false))
})
}
// Curry update handler by update type
const spawnUpdate = updateType => file => {
const target = path.join(sourceDir, file)
// Bust cache.
delete require.cache[target]
update(`${chalk.red(updateType)} ${chalk.cyan(file)}`)
}
// watch add/remove of files
const pagesWatcher = chokidar.watch([
'**/*.md',
'.vuepress/components/**/*.vue'
], {
cwd: sourceDir,
ignored: ['.vuepress/**/*.md', 'node_modules'],
ignoreInitial: true
})
pagesWatcher.on('add', spawnUpdate('add'))
pagesWatcher.on('unlink', spawnUpdate('unlink'))
pagesWatcher.on('addDir', spawnUpdate('addDir'))
pagesWatcher.on('unlinkDir', spawnUpdate('unlinkDir'))
const watchFiles = [
'.vuepress/config.js',
'.vuepress/config.yml',
'.vuepress/config.toml'
].concat(
(
ctx.siteConfig.extraWatchFiles || []
).map(file => normalizeWatchFilePath(file, ctx.sourceDir))
)
logger.debug('watchFiles', watchFiles)
// watch config file
const configWatcher = chokidar.watch(watchFiles, {
cwd: sourceDir,
ignoreInitial: true
})
configWatcher.on('change', spawnUpdate('change'))
// also listen for frontmatter changes from markdown files
frontmatterEmitter.on('update', () => update('frontmatter or headers change'))
// resolve webpack config
let config = createClientConfig(ctx)
config
.plugin('html')
// using a fork of html-webpack-plugin to avoid it requiring webpack
// internals from an incompatible version.
.use(require('vuepress-html-webpack-plugin'), [{
template: ctx.devTemplate
}])
config
.plugin('site-data')
.use(HeadPlugin, [{
tags: ctx.siteConfig.head || []
}])
const port = await resolvePort(cliOptions.port || ctx.siteConfig.port)
const { host, displayHost } = await resolveHost(cliOptions.host || ctx.siteConfig.host)
// debug in a running dev process.
process.stdin
&& process.stdin.on('data', chunk => {
const parsed = chunk.toString('utf-8').trim()
if (parsed === '*') {
console.log(Object.keys(ctx))
}
if (ctx[parsed]) {
console.log(ctx[parsed])
}
})
config
.plugin('vuepress-log')
.use(DevLogPlugin, [{
port,
displayHost,
publicPath: ctx.base
}])
config = config.toConfig()
const userConfig = ctx.siteConfig.configureWebpack
if (userConfig) {
config = applyUserWebpackConfig(userConfig, config, false /* isServer */)
}
const contentBase = path.resolve(sourceDir, '.vuepress/public')
const serverConfig = Object.assign({
disableHostCheck: true,
compress: true,
clientLogLevel: 'error',
hot: true,
quiet: true,
headers: {
'access-control-allow-origin': '*'
},
open: cliOptions.open,
publicPath: ctx.base,
watchOptions: {
ignored: [
/node_modules/,
`!${ctx.tempPath}/**`
]
},
historyApiFallback: {
disableDotRule: true,
rewrites: [
{ from: /./, to: path.posix.join(ctx.base, 'index.html') }
]
},
overlay: false,
host,
contentBase,
before (app, server) {
if (fs.existsSync(contentBase)) {
app.use(ctx.base, require('express').static(contentBase))
}
ctx.pluginAPI.options.beforeDevServer.syncApply(app, server)
},
after (app, server) {
ctx.pluginAPI.options.afterDevServer.syncApply(app, server)
}
}, ctx.siteConfig.devServer || {})
WebpackDevServer.addDevServerEntrypoints(config, serverConfig)
const compiler = webpack(config)
const server = new WebpackDevServer(compiler, serverConfig)
return {
server,
host,
port,
ctx
}
}
function resolveHost (host) {
const defaultHost = 'localhost'
host = host || defaultHost
const displayHost = host === defaultHost
? 'localhost'
: host
return {
displayHost,
host
}
}
async function resolvePort (port) {
const portfinder = require('portfinder')
portfinder.basePort = parseInt(port) || 8080
port = await portfinder.getPortPromise()
return port
}
function normalizeWatchFilePath (filepath, baseDir) {
const { isAbsolute, relative } = require('path')
if (isAbsolute(filepath)) {
return relative(baseDir, filepath)
}
return filepath
}
+23 -2
View File
@@ -1,5 +1,26 @@
'use strict'
exports.dev = require('./dev')
exports.build = require('./build')
const App = require('./node/App')
const { logger } = require('@vuepress/shared-utils')
function createApp (options) {
logger.wait('Extracting site metadata...')
return new App(options)
}
async function dev (options) {
const app = createApp(options)
await app.process()
await app.dev()
}
async function build (options) {
const app = createApp(options)
await app.process()
await app.build()
}
exports.createApp = createApp
exports.dev = dev
exports.build = build
exports.eject = require('./eject')
@@ -16,18 +16,21 @@ const {
const Page = require('./Page')
const ClientComputedMixin = require('./ClientComputedMixin')
const PluginAPI = require('../plugin-api/index')
const PluginAPI = require('./plugin-api')
const DevProcess = require('./dev')
const BuildProcess = require('./build')
const createTemp = require('./createTemp')
/**
* Expose AppContext.
* Expose VuePressApp.
*/
module.exports = class AppContext {
module.exports = class App {
static getInstance (...args) {
if (!AppContext._instance) {
AppContext._instance = new AppContext(...args)
if (!App._instance) {
App._instance = new App(...args)
}
return AppContext._instance
return App._instance
}
/**
@@ -35,24 +38,23 @@ module.exports = class AppContext {
*
* @param {string} sourceDir
* @param {{
* isProd: boolean,
* plugins: pluginsConfig,
* theme: themeNameConfig
* temp: string
* }} options
*/
constructor (sourceDir, cliOptions = {}, isProd) {
logger.debug('sourceDir', sourceDir)
this.sourceDir = sourceDir
this.cliOptions = cliOptions
this.isProd = isProd
constructor (options = {}) {
this.options = options
this.sourceDir = this.options.sourceDir || path.join(__dirname, 'docs.fallback')
logger.debug('sourceDir', this.sourceDir)
const { tempPath, writeTemp } = createTemp(cliOptions.temp)
const { tempPath, writeTemp } = createTemp(options.temp)
this.tempPath = tempPath
this.writeTemp = writeTemp
this.vuepressDir = path.resolve(sourceDir, '.vuepress')
this.vuepressDir = path.resolve(this.sourceDir, '.vuepress')
this.libDir = path.join(__dirname, '../')
}
/**
@@ -63,9 +65,14 @@ module.exports = class AppContext {
*/
resolveConfigAndInitialize () {
this.siteConfig = loadConfig(this.vuepressDir)
if (isFunction(this.siteConfig)) {
this.siteConfig = this.siteConfig(this)
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.
@@ -74,7 +81,7 @@ module.exports = class AppContext {
this.base = this.siteConfig.base || '/'
this.themeConfig = this.siteConfig.themeConfig || {}
const rawOutDir = this.cliOptions.dest || this.siteConfig.dest
const rawOutDir = this.options.dest || this.siteConfig.dest
this.outDir = rawOutDir
? require('path').resolve(this.cwd, rawOutDir)
: require('path').resolve(this.sourceDir, '.vuepress/dist')
@@ -92,7 +99,6 @@ module.exports = class AppContext {
async process () {
this.resolveConfigAndInitialize()
this.resolveCacheLoaderOptions()
this.normalizeHeadTagUrls()
this.themeAPI = loadTheme(this)
this.resolveTemplates()
@@ -105,18 +111,18 @@ module.exports = class AppContext {
this.markdown = createMarkdown(this)
await this.resolvePages()
await this.pluginAPI.options.additionalPages.apply(this)
await this.pluginAPI.applyAsyncOption('additionalPages', this)
await Promise.all(
this.pluginAPI.options.additionalPages.appliedValues.map(async (options) => {
this.pluginAPI.getOption('additionalPages').appliedValues.map(async (options) => {
await this.addPage(options)
})
)
await this.pluginAPI.options.ready.apply()
await this.pluginAPI.applyAsyncOption('ready')
await Promise.all([
this.pluginAPI.options.clientDynamicModules.apply(this),
this.pluginAPI.options.enhanceAppFiles.apply(this),
this.pluginAPI.options.globalUIComponents.apply(this)
this.pluginAPI.applyAsyncOption('clientDynamicModules', this),
this.pluginAPI.applyAsyncOption('enhanceAppFiles', this),
this.pluginAPI.applyAsyncOption('globalUIComponents', this)
])
}
@@ -138,17 +144,17 @@ module.exports = class AppContext {
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(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('@vuepress/container', {
type: 'slot',
before: info => `<template slot="${info}">`,
@@ -176,7 +182,7 @@ module.exports = class AppContext {
*/
applyUserPlugins () {
this.pluginAPI.useByPluginsConfig(this.cliOptions.plugins)
this.pluginAPI.useByPluginsConfig(this.options.plugins)
if (this.themeAPI.existsParentTheme) {
this.pluginAPI.use(this.themeAPI.parentTheme.entry)
}
@@ -215,7 +221,7 @@ module.exports = class AppContext {
*/
resolveCacheLoaderOptions () {
Object.assign(this, (getCacheLoaderOptions(this.siteConfig, this.cliOptions, this.cwd, this.isProd)))
Object.assign(this, (getCacheLoaderOptions(this.siteConfig, this.options, this.cwd, this.isProd)))
}
/**
@@ -235,7 +241,7 @@ module.exports = class AppContext {
this.devTemplate = this.resolveCommonAgreementFilePath(
'devTemplate',
{
defaultValue: path.resolve(__dirname, '../app/index.dev.html'),
defaultValue: this.getLibFilePath('client/index.dev.html'),
siteAgreement: 'templates/dev.html',
themeAgreement: 'templates/dev.html'
}
@@ -244,7 +250,7 @@ module.exports = class AppContext {
this.ssrTemplate = this.resolveCommonAgreementFilePath(
'ssrTemplate',
{
defaultValue: path.resolve(__dirname, '../app/index.ssr.html'),
defaultValue: this.getLibFilePath('client/index.ssr.html'),
siteAgreement: 'templates/ssr.html',
themeAgreement: 'templates/ssr.html'
}
@@ -265,7 +271,7 @@ module.exports = class AppContext {
this.globalLayout = this.resolveCommonAgreementFilePath(
'globalLayout',
{
defaultValue: path.resolve(__dirname, `../app/components/GlobalLayout.vue`),
defaultValue: this.getLibFilePath('client/components/GlobalLayout.vue'),
siteAgreement: `components/GlobalLayout.vue`,
themeAgreement: `layouts/GlobalLayout.vue`
}
@@ -343,7 +349,7 @@ module.exports = class AppContext {
await page.process({
markdown: this.markdown,
computed: new this.ClientComputedMixinConstructor(),
enhancers: this.pluginAPI.options.extendPageData.items
enhancers: this.pluginAPI.getOption('extendPageData').items
})
this.pages.push(page)
}
@@ -425,45 +431,52 @@ module.exports = class AppContext {
locales
}
}
/**
* Get file path in core lib
*
* @param relative
* @returns {string}
* @api public
*/
getLibFilePath (relative) {
return path.join(this.libDir, relative)
}
/**
* Start a dev process with correct app context
*
* @returns {Promise<void>}
* @api public
*/
async dev () {
this.isProd = false
this.devProcess = new DevProcess(this)
await this.devProcess.process()
this.devProcess
.on('fileChanged', ({ type, target }) => {
console.log(`Reload due to ${chalk.red(type)} ${chalk.cyan(target)}`)
this.process()
})
.createServer()
.listen()
}
/**
* Start a build process with correct app context
*
* @returns {Promise<void>}
* @api public
*/
async build () {
this.isProd = true
this.buildProcess = new BuildProcess(this)
await this.buildProcess.process()
await this.buildProcess.render()
}
}
/**
* Create a dynamic temp utility context that allow to lanuch
* multiple apps with isolated context at the same time.
* @param tempPath
* @returns {{
* writeTemp: (function(file: string, content: string): string),
* tempPath: string
* }}
*/
function createTemp (tempPath) {
if (!tempPath) {
tempPath = path.resolve(__dirname, '../../.temp')
} else {
tempPath = path.resolve(tempPath)
}
if (!fs.existsSync(tempPath)) {
fs.ensureDirSync(tempPath)
} else {
fs.emptyDirSync(tempPath)
}
logger.debug(`Temp directory: ${chalk.gray(tempPath)}`)
const tempCache = new Map()
async function writeTemp (file, content) {
const destPath = path.join(tempPath, file)
await fs.ensureDir(path.parse(destPath).dir)
// cache write to avoid hitting the dist if it didn't change
const cached = tempCache.get(file)
if (cached !== content) {
await fs.writeFile(destPath, content)
tempCache.set(file, content)
}
return destPath
}
return { writeTemp, tempPath }
}
@@ -12,12 +12,12 @@ const {
/**
* Get cache directory and cache identifier via config.
* @param {object} siteConfig
* @param {object} cliOptions
* @param {object} options
*/
exports.getCacheLoaderOptions = function (siteConfig, cliOptions, cwd, isProd) {
exports.getCacheLoaderOptions = function (siteConfig, options, cwd, isProd) {
const defaultCacheDirectory = path.resolve(__dirname, '../../node_modules/.cache/vuepress')
let cache = cliOptions.cache || siteConfig.cache || defaultCacheDirectory
let cache = options.cache || siteConfig.cache || defaultCacheDirectory
if (isBoolean(cache)) {
if (cache === true) {
@@ -4,7 +4,7 @@
* Module dependencies.
*/
const { inferDate, DATE_RE } = require('../util/index')
const { inferDate, DATE_RE } = require('./util/index')
const {
fs,
path,
@@ -1,4 +1,4 @@
const AsyncOption = require('../../lib/plugin-api/abstract/AsyncOption')
const AsyncOption = require('../../plugin-api/abstract/AsyncOption')
describe('AsyncOption', () => {
test('parallelApply', async () => {
@@ -1,4 +1,4 @@
import Option from '../../lib/plugin-api/abstract/Option'
import Option from '../../plugin-api/abstract/Option'
describe('Option', () => {
test('key', () => {
@@ -2,8 +2,8 @@ jest.mock('vuepress-plugin-a')
jest.mock('vuepress-plugin-b')
jest.mock('@org/vuepress-plugin-a')
import PluginAPI from '../../lib/plugin-api/index'
import { PLUGIN_OPTION_MAP } from '../../lib/plugin-api/constants'
import PluginAPI from '../../plugin-api/index'
import { PLUGIN_OPTION_MAP } from '../../plugin-api/constants'
describe('Plugin', () => {
test('registerOption', () => {
@@ -1,31 +1,31 @@
import { flattenPlugin } from '../../lib/plugin-api/util'
import { flattenPlugin } from '../../plugin-api/util'
describe('flattenPlugin', () => {
test('should hydrate plugin correctly', () => {
const plugin = { name: 'a', shortcut: 'a', module: { enhanceAppFiles: 'file' }}
const plugin = { name: 'a', shortcut: 'a', entry: { enhanceAppFiles: 'file' }}
const hydratedPlugin = flattenPlugin(plugin, {}, {})
expect(hydratedPlugin.name).toBe('a')
expect(hydratedPlugin.shortcut).toBe('a')
expect(hydratedPlugin.shortcut).toBe(null)
expect(hydratedPlugin.enabled).toBe(true)
expect(hydratedPlugin.enhanceAppFiles).toBe('file')
})
test('should set \'enabled\' to false when \'pluginOptions\' is set to false.', () => {
const plugin = { name: 'a', shortcut: 'a', module: {}}
const plugin = { name: 'a', shortcut: 'a', entry: {}}
const hydratedPlugin = flattenPlugin(plugin, false, {})
expect(hydratedPlugin.name).toBe('a')
expect(hydratedPlugin.shortcut).toBe('a')
expect(hydratedPlugin.shortcut).toBe(null)
expect(hydratedPlugin.enabled).toBe(false)
})
test('should flatten functional plugin correctly.', () => {
const config = jest.fn(() => ({ enhanceAppFiles: 'file' }))
const plugin = { name: 'a', shortcut: 'a', module: config }
const plugin = { name: 'a', shortcut: 'a', entry: config }
const pluginOptions = {}
const pluginContext = {}
const hydratedPlugin = flattenPlugin(plugin, pluginOptions, pluginContext)
expect(hydratedPlugin.name).toBe('a')
expect(hydratedPlugin.shortcut).toBe('a')
expect(hydratedPlugin.shortcut).toBe(null)
expect(hydratedPlugin.enabled).toBe(true)
expect(hydratedPlugin.enhanceAppFiles).toBe('file')
expect(config.mock.calls).toHaveLength(1)
@@ -35,7 +35,7 @@ describe('flattenPlugin', () => {
test('should flatten functional plugin correctly - options defaults to \'{}\'.', () => {
const config = jest.fn(() => ({ enhanceAppFiles: 'file' }))
const plugin = { name: 'a', shortcut: 'a', module: config }
const plugin = { name: 'a', shortcut: 'a', entry: config }
const pluginOptions = undefined
const pluginContext = {}
flattenPlugin(plugin, pluginOptions, pluginContext)
@@ -1,5 +1,5 @@
const { fs, path } = require('@vuepress/shared-utils')
const prepare = require('../../lib/prepare')
const App = require('../../App')
const docsBaseDir = path.resolve(__dirname, 'fixtures')
const docsModeNames = fs.readdirSync(docsBaseDir)
@@ -9,12 +9,16 @@ const docsModes = docsModeNames.map(name => {
return { name, docsPath, docsTempPath }
})
describe('prepare', () => {
describe('App', () => {
test('should not throw error', async () => {
await Promise.all(docsModes.map(async ({ name, docsPath, docsTempPath }) => {
await fs.ensureDir(docsTempPath)
const context = await prepare(docsPath, { theme: '@vuepress/default', temp: docsTempPath })
expect(context.sourceDir).toBe(docsPath)
const app = new App({
sourceDir: docsPath,
theme: '@vuepress/default',
emp: docsTempPath
})
expect(app.sourceDir).toBe(docsPath)
}))
})
})
@@ -1,19 +1,28 @@
const Page = require('../../lib/prepare/Page')
const Page = require('../../Page')
const App = require('../../App')
const {
getComputed,
getMarkdown,
getDocument,
readFile
} = require('./util')
describe('Page', () => {
let app
let computed
beforeAll(async () => {
app = new App()
await app.process()
computed = new app.ClientComputedMixinConstructor()
})
test('pure route', async () => {
const page = new Page({ path: '/' })
const page = new Page({ path: '/' }, app)
expect(page.path).toBe('/')
expect(page.regularPath).toBe('/')
const computed = getComputed()
await page.process({ computed })
expect(page.path).toBe('/')
@@ -22,7 +31,7 @@ describe('Page', () => {
test('pure route - encodeURI', async () => {
const path = '/尤/'
const page = new Page({ path })
const page = new Page({ path }, app)
expect(page.path).toBe(encodeURI(path))
expect(page.regularPath).toBe(encodeURI(path))
@@ -33,7 +42,7 @@ describe('Page', () => {
const page = new Page({
path: '/',
frontmatter
})
}, app)
expect(page.frontmatter).toBe(frontmatter)
})
@@ -42,15 +51,16 @@ describe('Page', () => {
const page = new Page({
path: '/',
frontmatter
})
}, app)
expect(page.frontmatter.title).toBe('alpha')
const computed = getComputed()
const enhancers = [
{
name: 'plugin-a',
value: page => { page.frontmatter.title = 'beta' }
value: page => {
page.frontmatter.title = 'beta'
}
}
]
await page.process({ computed, enhancers })
@@ -60,14 +70,13 @@ describe('Page', () => {
test('markdown page - pointing to a markdown file', async () => {
const { relative, filePath } = getDocument('README.md')
const page = new Page({ filePath, relative })
const page = new Page({ filePath, relative }, app)
expect(page._filePath).toBe(filePath)
expect(page.regularPath).toBe('/')
expect(page.path).toBe('/')
expect(page.frontmatter).toEqual({})
const computed = getComputed()
const markdown = getMarkdown()
await page.process({ computed, markdown })
@@ -79,14 +88,13 @@ describe('Page', () => {
test('markdown page - pointing to a markdown file with frontmatter', async () => {
const { relative, filePath } = getDocument('alpha.md')
const page = new Page({ filePath, relative })
const page = new Page({ filePath, relative }, app)
expect(page._filePath).toBe(filePath)
expect(page.regularPath).toBe('/alpha.html')
expect(page.path).toBe('/alpha.html')
expect(page.frontmatter).toEqual({})
const computed = getComputed()
const markdown = getMarkdown()
await page.process({ computed, markdown })
@@ -1,15 +1,5 @@
const { fs, path } = require('@vuepress/shared-utils')
const AppContext = require('../../lib/prepare/AppContext')
const createMarkdown = require('../../../markdown/index')
function getAppContext () {
return new AppContext('.')
}
function getComputed () {
const context = getAppContext()
return new context.ClientComputedMixinConstructor()
}
const createMarkdown = require('../../../../../markdown/index')
const docsBaseDir = path.resolve(__dirname, 'fixtures/docs')
@@ -25,8 +15,6 @@ const getMarkdown = createMarkdown
const readFile = async filePath => await fs.readFile(filePath, 'utf-8')
module.exports = {
getAppContext,
getComputed,
getMarkdown,
getDocument,
readFile
@@ -1,7 +1,7 @@
jest.mock('vuepress-theme-parent')
jest.mock('vuepress-theme-child')
import ThemeAPI from '../../lib/theme-api'
import ThemeAPI from '../../theme-api'
import { resolve } from 'path'
const theme = {
@@ -0,0 +1,273 @@
'use strict'
const EventEmitter = require('events').EventEmitter
const webpack = require('webpack')
const readline = require('readline')
const escape = require('escape-html')
const { chalk, fs, path, logger, env, performance } = require('@vuepress/shared-utils')
const createClientConfig = require('../webpack/createClientConfig')
const createServerConfig = require('../webpack/createServerConfig')
const { createBundleRenderer } = require('vue-server-renderer')
const { normalizeHeadTag, applyUserWebpackConfig } = require('../util/index')
/**
* Expose Build Process Class.
*/
module.exports = class Build extends EventEmitter {
constructor (context) {
super()
process.env.NODE_ENV = 'production'
this.context = context
this.outDir = this.context.outDir
}
/**
* Doing somthing before render pages, e.g. validate and empty output directory,
* prepare webpack config.
*
* @returns {Promise<void>}
* @api public
*/
async process () {
if (this.context.cwd === this.outDir) {
throw new Error('Unexpected option: "outDir" cannot be set to the current working directory')
}
this.context.resolveCacheLoaderOptions()
await fs.emptyDir(this.outDir)
logger.debug('Dist directory: ' + chalk.gray(this.outDir))
this.prepareWebpackConfig()
}
/**
* Compile and render pages.
*
* @returns {Promise<void>}
* @api public
*/
async render () {
logger.wait('Extracting site metadata...')
// compile!
const stats = await compile([this.clientConfig, this.serverConfig])
const serverBundle = require(path.resolve(this.outDir, 'manifest/server.json'))
const clientManifest = require(path.resolve(this.outDir, 'manifest/client.json'))
// remove manifests after loading them.
await fs.remove(path.resolve(this.outDir, 'manifest'))
// ref: https://github.com/vuejs/vuepress/issues/1367
if (!this.clientConfig.devtool && (!this.clientConfig.plugins
|| !this.clientConfig.plugins.some(p =>
p instanceof webpack.SourceMapDevToolPlugin
|| p instanceof webpack.EvalSourceMapDevToolPlugin
))) {
await workaroundEmptyStyleChunk(stats, this.outDir)
}
// create server renderer using built manifests
this.renderer = createBundleRenderer(serverBundle, {
clientManifest,
runInNewContext: false,
inject: false,
shouldPrefetch: this.context.siteConfig.shouldPrefetch || (() => true),
template: await fs.readFile(this.context.ssrTemplate, 'utf-8')
})
// pre-render head tags from user config
this.userHeadTags = (this.context.siteConfig.head || [])
.map(renderHeadTag)
.join('\n ')
// if the user does not have a custom 404.md, generate the theme's default
if (!this.context.pages.some(p => p.path === '/404.html')) {
this.context.addPage({ path: '/404.html' })
}
// render pages
logger.wait('Rendering static HTML...')
const pagePaths = []
for (const page of this.context.pages) {
pagePaths.push(await this.renderPage(page))
}
readline.clearLine(process.stdout, 0)
readline.cursorTo(process.stdout, 0)
await this.context.pluginAPI.applyAsyncOption('generated', pagePaths)
// DONE.
const relativeDir = path.relative(this.context.cwd, this.outDir)
logger.success(`Generated static files in ${chalk.cyan(relativeDir)}.`)
const { duration } = performance.stop()
logger.developer(`It took a total of ${chalk.cyan(`${duration}ms`)} to run the ${chalk.cyan('vuepress build')}.`)
console.log()
}
/**
* Prepare webpack config under build.
*
* @api private
*/
prepareWebpackConfig () {
this.clientConfig = createClientConfig(this.context).toConfig()
this.serverConfig = createServerConfig(this.context).toConfig()
const userConfig = this.context.siteConfig.configureWebpack
if (userConfig) {
this.clientConfig = applyUserWebpackConfig(userConfig, this.clientConfig, false)
this.serverConfig = applyUserWebpackConfig(userConfig, this.serverConfig, true)
}
}
/**
* Render page
*
* @param {Page} page
* @returns {Promise<string>}
* @api private
*/
async renderPage (page) {
const pagePath = page.path
readline.clearLine(process.stdout, 0)
readline.cursorTo(process.stdout, 0)
process.stdout.write(`Rendering page: ${pagePath}`)
// #565 Avoid duplicate description meta at SSR.
const meta = (page.frontmatter && page.frontmatter.meta || []).filter(item => item.name !== 'description')
const pageMeta = renderPageMeta(meta)
const context = {
url: pagePath,
userHeadTags: this.userHeadTags,
pageMeta,
title: 'VuePress',
lang: 'en',
description: ''
}
let html
try {
html = await this.renderer.renderToString(context)
} catch (e) {
console.error(logger.error(chalk.red(`Error rendering ${pagePath}:`), false))
throw e
}
const filename = decodeURIComponent(pagePath.replace(/\/$/, '/index.html').replace(/^\//, ''))
const filePath = path.resolve(this.outDir, filename)
await fs.ensureDir(path.dirname(filePath))
await fs.writeFile(filePath, html)
return filePath
}
}
/**
* Compile a webpack application and return stats json.
*
* @param {Object} config
* @returns {Promise<Object>}
*/
function compile (config) {
return new Promise((resolve, reject) => {
webpack(config, (err, stats) => {
if (err) {
return reject(err)
}
if (stats.hasErrors()) {
stats.toJson().errors.forEach(err => {
console.error(err)
})
reject(new Error(`Failed to compile with errors.`))
return
}
if (env.isDebug && stats.hasWarnings()) {
stats.toJson().warnings.forEach(warning => {
console.warn(warning)
})
}
resolve(stats.toJson({ modules: false }))
})
})
}
/**
* Render head tag
*
* @param {Object} tag
* @returns {string}
*/
function renderHeadTag (tag) {
const { tagName, attributes, innerHTML, closeTag } = normalizeHeadTag(tag)
return `<${tagName}${renderAttrs(attributes)}>${innerHTML}${closeTag ? `</${tagName}>` : ``}`
}
/**
* Render html attributes
*
* @param {Object} attrs
* @returns {string}
*/
function renderAttrs (attrs = {}) {
const keys = Object.keys(attrs)
if (keys.length) {
return ' ' + keys.map(name => `${name}="${escape(attrs[name])}"`).join(' ')
} else {
return ''
}
}
/**
* Render meta tags
*
* @param {Array} meta
* @returns {Array<string>}
*/
function renderPageMeta (meta) {
if (!meta) return ''
return meta.map(m => {
let res = `<meta`
Object.keys(m).forEach(key => {
res += ` ${key}="${escape(m[key])}"`
})
return res + `>`
}).join('')
}
/**
* find and remove empty style chunk caused by
* https://github.com/webpack-contrib/mini-css-extract-plugin/issues/85
* TODO remove when it's fixed
*
* @param {Object} stats
* @param {String} outDir
* @returns {Promise<void>}
*/
async function workaroundEmptyStyleChunk (stats, outDir) {
const styleChunk = stats.children[0].assets.find(a => {
return /styles\.\w{8}\.js$/.test(a.name)
})
if (!styleChunk) return
const styleChunkPath = path.resolve(outDir, styleChunk.name)
const styleChunkContent = await fs.readFile(styleChunkPath, 'utf-8')
await fs.remove(styleChunkPath)
// prepend it to app.js.
// this is necessary for the webpack runtime to work properly.
const appChunk = stats.children[0].assets.find(a => {
return /app\.\w{8}\.js$/.test(a.name)
})
const appChunkPath = path.resolve(outDir, appChunk.name)
const appChunkContent = await fs.readFile(appChunkPath, 'utf-8')
await fs.writeFile(appChunkPath, styleChunkContent + appChunkContent)
}
@@ -16,12 +16,12 @@ module.exports = function (ctx) {
const beforeInstantiate = config => {
chainMarkdown && chainMarkdown(config)
ctx.pluginAPI.options.chainMarkdown.syncApply(config)
ctx.pluginAPI.applySyncOption('chainMarkdown', config)
}
const afterInstantiate = md => {
extendMarkdown && extendMarkdown(md)
ctx.pluginAPI.options.extendMarkdown.syncApply(md)
ctx.pluginAPI.applySyncOption('extendMarkdown', md)
}
return createMarkdown(
@@ -0,0 +1,42 @@
const { fs, path, chalk, logger } = require('@vuepress/shared-utils')
/**
* Create a dynamic temp utility context that allow to lanuch
* multiple apps with isolated context at the same time.
* @param tempPath
* @returns {{
* writeTemp: (function(file: string, content: string): string),
* tempPath: string
* }}
*/
module.exports = function createTemp (tempPath) {
if (!tempPath) {
tempPath = path.resolve(__dirname, '../../.temp')
} else {
tempPath = path.resolve(tempPath)
}
if (!fs.existsSync(tempPath)) {
fs.ensureDirSync(tempPath)
} else {
fs.emptyDirSync(tempPath)
}
logger.debug(`Temp directory: ${chalk.gray(tempPath)}`)
const tempCache = new Map()
async function writeTemp (file, content) {
const destPath = path.join(tempPath, file)
await fs.ensureDir(path.parse(destPath).dir)
// cache write to avoid hitting the dist if it didn't change
const cached = tempCache.get(file)
if (cached !== content) {
await fs.writeFile(destPath, content)
tempCache.set(file, content)
}
return destPath
}
return { writeTemp, tempPath }
}
@@ -0,0 +1,272 @@
'use strict'
/**
* Module dependencies.
*/
const EventEmitter = require('events').EventEmitter
const WebpackDevServer = require('webpack-dev-server')
const { frontmatterEmitter } = require('@vuepress/markdown-loader')
const webpack = require('webpack')
const chokidar = require('chokidar')
const { path, fs, logger } = require('@vuepress/shared-utils')
const HeadPlugin = require('../webpack/HeadPlugin')
const DevLogPlugin = require('../webpack/DevLogPlugin')
const createClientConfig = require('../webpack/createClientConfig')
const { applyUserWebpackConfig } = require('../util/index')
module.exports = class DevProcess extends EventEmitter {
constructor (context) {
super()
this.context = context
}
/**
* Prepare essential data for launch dev server.
*/
async process () {
this.context.resolveCacheLoaderOptions()
this.watchSourceFiles()
this.watchUserConfig()
this.watchFrontmatter()
this.setupDebugTip()
await this.resolvePort()
await this.resolveHost()
this.prepareWebpackConfig()
return this
}
/**
* Hande file's update, need to re-prepare app context.
*
* @param {string} type
* @param {string} target
*/
handleUpdate (type, target) {
if (!path.isAbsolute(target)) {
target = path.join(this.context.sourceDir, target)
}
if (target.endsWith('.js')) {
// Bust cache.
delete require.cache[target]
}
this.emit('fileChanged', {
type,
target
})
}
/**
* Watch user's source document files.
*/
watchSourceFiles () {
// watch add/remove of files
this.pagesWatcher = chokidar.watch([
'**/*.md',
'.vuepress/components/**/*.vue'
], {
cwd: this.context.sourceDir,
ignored: ['.vuepress/**/*.md', 'node_modules'],
ignoreInitial: true
})
this.pagesWatcher.on('add', target => this.handleUpdate('add', target))
this.pagesWatcher.on('unlink', target => this.handleUpdate('unlink', target))
this.pagesWatcher.on('addDir', target => this.handleUpdate('addDir', target))
this.pagesWatcher.on('unlinkDir', target => this.handleUpdate('unlinkDir', target))
}
/**
* Watch user's config files and extra files.
*/
watchUserConfig () {
this.watchFiles = [
'.vuepress/config.js',
'.vuepress/config.yml',
'.vuepress/config.toml'
].concat(
(
this.context.siteConfig.extraWatchFiles || []
).map(file => normalizeWatchFilePath(file, this.context.sourceDir))
)
logger.debug('watchFiles', this.watchFiles)
this.configWatcher = chokidar.watch(this.watchFiles, {
cwd: this.context.sourceDir,
ignoreInitial: true
})
this.configWatcher.on('change', target => this.handleUpdate('change', target))
}
/**
* Also listen for frontmatter changes from markdown files
*/
watchFrontmatter () {
frontmatterEmitter.on('update', target => this.handleUpdate('frontmatter', target))
}
/**
* Resolve used port
*/
async resolvePort () {
this.port = await resolvePort(this.context.options.port || this.context.siteConfig.port)
}
/**
* Resolve used host
*/
async resolveHost () {
const { host, displayHost } = await resolveHost(this.context.options.host || this.context.siteConfig.host)
this.host = host
this.displayHost = displayHost
}
/**
* Set up a shortcut to debug context under dev.
*/
setupDebugTip () {
// debug in a running dev process.
process.stdin
&& process.stdin.on('data', chunk => {
const parsed = chunk.toString('utf-8').trim()
if (parsed === '*') {
console.log(Object.keys(this.context))
}
if (this.context[parsed]) {
console.log(this.context[parsed])
}
})
}
/**
* Prepare webpack for dev process.
*/
prepareWebpackConfig () {
// resolve webpack config
let config = createClientConfig(this.context)
config
.plugin('html')
// using a fork of html-webpack-plugin to avoid it requiring webpack
// internals from an incompatible version.
.use(require('vuepress-html-webpack-plugin'), [{
template: this.context.devTemplate
}])
config
.plugin('site-data')
.use(HeadPlugin, [{
tags: this.context.siteConfig.head || []
}])
config
.plugin('vuepress-log')
.use(DevLogPlugin, [{
port: this.port,
displayHost: this.displayHost,
publicPath: this.context.base
}])
config = config.toConfig()
const userConfig = this.context.siteConfig.configureWebpack
if (userConfig) {
config = applyUserWebpackConfig(userConfig, config, false /* isServer */)
}
this.webpackConfig = config
}
createServer () {
const contentBase = path.resolve(this.context.sourceDir, '.vuepress/public')
const serverConfig = Object.assign({
disableHostCheck: true,
compress: true,
clientLogLevel: 'error',
hot: true,
quiet: true,
headers: {
'access-control-allow-origin': '*'
},
open: this.context.options.open,
publicPath: this.context.base,
watchOptions: {
ignored: [
/node_modules/,
`!${this.context.tempPath}/**`
]
},
historyApiFallback: {
disableDotRule: true,
rewrites: [
{ from: /./, to: path.posix.join(this.context.base, 'index.html') }
]
},
overlay: false,
host: this.host,
contentBase,
before: (app, server) => {
if (fs.existsSync(contentBase)) {
app.use(this.context.base, require('express').static(contentBase))
}
this.context.pluginAPI.applySyncOption('beforeDevServer', app, server)
},
after: (app, server) => {
this.context.pluginAPI.applySyncOption('afterDevServer', app, server)
}
}, this.context.siteConfig.devServer || {})
WebpackDevServer.addDevServerEntrypoints(this.webpackConfig, serverConfig)
const compiler = webpack(this.webpackConfig)
this.server = new WebpackDevServer(compiler, serverConfig)
return this
}
listen () {
this.server.listen(this.port, this.host, err => {
if (err) {
console.log(err)
}
})
return this
}
}
function resolveHost (host) {
const defaultHost = 'localhost'
host = host || defaultHost
const displayHost = host === defaultHost
? 'localhost'
: host
return {
displayHost,
host
}
}
async function resolvePort (port) {
const portfinder = require('portfinder')
portfinder.basePort = parseInt(port) || 8080
port = await portfinder.getPortPromise()
return port
}
function normalizeWatchFilePath (filepath, baseDir) {
const { isAbsolute, relative } = require('path')
if (isAbsolute(filepath)) {
return relative(baseDir, filepath)
}
return filepath
}
@@ -0,0 +1,3 @@
# VuePress
> `Prompts`: You are running VuePress without setting sourceDir!
@@ -18,7 +18,7 @@ module.exports = function (source, map) {
&& parsed.data
&& JSON.stringify(cached.data) !== JSON.stringify(parsed.data)
) {
frontmatterEmitter.emit('update')
frontmatterEmitter.emit('update', file)
}
cache.set(file, parsed)
@@ -8,7 +8,7 @@ module.exports = (options, ctx) => ({
async ready () {
// 1. enable config.styl globally.
const configFile = path.resolve(__dirname, '../../app/style/config.styl')
const configFile = ctx.getLibFilePath('client/style/config.styl')
if (!ctx.siteConfig.stylus) {
ctx.siteConfig.stylus = {
import: [configFile]
@@ -1,4 +1,3 @@
const { path } = require('@vuepress/shared-utils')
const { codegen: { pathsToModuleCode }} = require('@vuepress/shared-utils')
module.exports = (options, context, api) => ({
@@ -7,7 +6,7 @@ module.exports = (options, context, api) => ({
// @internal/root-mixins
async clientDynamicModules () {
const builtInRootMixins = [
path.resolve(__dirname, '../app/root-mixins/updateMeta.js')
context.getLibFilePath('client/root-mixins/updateMeta.js')
]
const rootMixins = [
@@ -11,7 +11,7 @@ module.exports = (options, ctx) => ({
async clientDynamicModules () {
const files = [
path.resolve(__dirname, '../prepare/ClientComputedMixin.js')
path.resolve(__dirname, '../ClientComputedMixin.js')
]
const modules = await Promise.all(files.map(async file => {
@@ -11,7 +11,7 @@ const {
datatypes: { isString },
logger, chalk
} = require('@vuepress/shared-utils')
const ThemeAPI = require('../theme-api')
const ThemeAPI = require('./theme-api')
/**
* Resolve theme.
@@ -37,14 +37,16 @@ module.exports = function loadTheme (ctx) {
if (!theme.path) {
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`)
}
logger.tip(`Apply theme ${chalk.gray(theme.name)}`)
let applyTip = `Apply theme ${chalk.magenta(theme.name)}`
theme.entry.name = '@vuepress/internal-theme-entry-file'
let parentTheme = {}
if (theme.entry.extend) {
parentTheme = resolveTheme(ctx, themeResolver, true, theme.entry.extend)
parentTheme.entry.name = '@vuepress/internal-parent-theme-entry-file'
applyTip += chalk.gray(` (extends ${chalk.magenta(parentTheme.name)})`)
}
logger.tip(applyTip + ' ...')
logger.debug('theme', theme.name, theme.path)
logger.debug('parentTheme', parentTheme.name, parentTheme.path)
@@ -73,9 +75,9 @@ function normalizeThemePath (resolved) {
}
function resolveTheme (ctx, resolver, ignoreLocal, theme) {
const { siteConfig, cliOptions, sourceDir, vuepressDir, pluginAPI } = ctx
const { siteConfig, options, sourceDir, vuepressDir, pluginAPI } = ctx
const localThemePath = resolve(vuepressDir, 'theme')
theme = theme || siteConfig.theme || cliOptions.theme
theme = theme || siteConfig.theme || options.theme
let path
let name
@@ -238,6 +238,50 @@ module.exports = class PluginAPI {
.registerOption(PLUGIN_OPTION_MAP.BEFORE_DEV_SERVER.key, beforeDevServer, pluginName)
.registerOption(PLUGIN_OPTION_MAP.AFTER_DEV_SERVER.key, afterDevServer, pluginName)
}
/**
* Apply synchronous option.
*
* @param {string} name
* @param {Array<any>} args
* @returns {void}
* @api public
*/
applySyncOption (name, ...args) {
logger.debug('applySyncOption: ' + name)
this.getOption(name).apply(...args)
return this
}
/**
* Apply asynchronous option.
*
* @param {string} name
* @param {Array<any>} args
* @returns {Promise<void>}
* @api public
*/
async applyAsyncOption (name, ...args) {
logger.debug('applyAsyncOption: ' + name)
await this.getOption(name).apply(...args)
}
/**
* Get exisiting option
*
* @param name
* @returns {Option}
* @api public
*/
getOption (name) {
if (!this.options[name]) {
throw new Error(`Unknown option ${name}`)
}
return this.options[name]
}
}
function pluginLog (name, shortcut) {
@@ -0,0 +1,3 @@
<template>
<Content/>
</template>
@@ -88,7 +88,7 @@ module.exports = class ThemeAPI {
layoutComponentMap.NotFound = {
filename: 'NotFound.vue',
componentName: 'NotFound',
path: resolve(__dirname, '../app/components/NotFound.vue'),
path: resolve(__dirname, '../../client/components/NotFound.vue'),
isInternal: true
}
}
@@ -10,20 +10,22 @@ const { fs, path, logger, env } = require('@vuepress/shared-utils')
* Expose createBaseConfig method.
*/
module.exports = function createBaseConfig ({
siteConfig,
sourceDir,
outDir,
base: publicPath,
markdown,
tempPath,
cacheDirectory,
cacheIdentifier,
cliOptions: {
cache
},
pluginAPI
}, isServer) {
module.exports = function createBaseConfig (context, isServer) {
const {
siteConfig,
sourceDir,
outDir,
base: publicPath,
markdown,
tempPath,
cacheDirectory,
cacheIdentifier,
options: {
cache
},
pluginAPI
} = context
const Config = require('webpack-chain')
const { VueLoaderPlugin } = require('vue-loader')
const CSSExtractPlugin = require('mini-css-extract-plugin')
@@ -47,12 +49,14 @@ module.exports = function createBaseConfig ({
}
const modulePaths = getModulePaths()
const clientDir = context.getLibFilePath('client')
config.resolve
.set('symlinks', true)
.alias
.set('@source', sourceDir)
.set('@app', path.resolve(__dirname, '../app'))
.set('@client', clientDir)
.set('@app', clientDir)
.set('@temp', tempPath)
.set('@dynamic', path.resolve(tempPath, 'dynamic'))
.set('@internal', path.resolve(tempPath, 'internal'))
@@ -76,7 +80,7 @@ module.exports = function createBaseConfig ({
fs.emptyDirSync(cacheDirectory)
}
cacheIdentifier += `isServer:${isServer}`
const finalCacheIdentifier = cacheIdentifier + `isServer:${isServer}`
function applyVuePipeline (rule) {
rule
@@ -84,7 +88,7 @@ module.exports = function createBaseConfig ({
.loader('cache-loader')
.options({
cacheDirectory,
cacheIdentifier
cacheIdentifier: finalCacheIdentifier
})
rule
@@ -95,7 +99,7 @@ module.exports = function createBaseConfig ({
preserveWhitespace: true
},
cacheDirectory,
cacheIdentifier
cacheIdentifier: finalCacheIdentifier
})
}
@@ -144,7 +148,7 @@ module.exports = function createBaseConfig ({
.loader('cache-loader')
.options({
cacheDirectory,
cacheIdentifier
cacheIdentifier: finalCacheIdentifier
})
.end()
.use('babel-loader')
@@ -281,13 +285,13 @@ module.exports = function createBaseConfig ({
config
.plugin('injections')
.use(require('webpack/lib/DefinePlugin'), [{
VUEPRESS_VERSION: JSON.stringify(require('../../package.json').version),
VUEPRESS_VERSION: JSON.stringify(require('../../../package.json').version),
VUEPRESS_TEMP_PATH: JSON.stringify(tempPath),
LAST_COMMIT_HASH: JSON.stringify(getLastCommitHash())
}])
pluginAPI.options.define.apply(config)
pluginAPI.options.alias.apply(config)
pluginAPI.applySyncOption('define', config)
pluginAPI.applySyncOption('alias', config)
return config
}
@@ -5,14 +5,14 @@
*/
module.exports = function createClientConfig (ctx) {
const { path, env } = require('@vuepress/shared-utils')
const { env } = require('@vuepress/shared-utils')
const createBaseConfig = require('./createBaseConfig')
const config = createBaseConfig(ctx)
config
.entry('app')
.add(path.resolve(__dirname, '../app/clientEntry.js'))
.add(ctx.getLibFilePath('client/clientEntry.js'))
config.node
.merge({
@@ -71,7 +71,7 @@ module.exports = function createClientConfig (ctx) {
}])
}
ctx.pluginAPI.options.chainWebpack.syncApply(config, false /* isServer */)
ctx.pluginAPI.applySyncOption('chainWebpack', config, false /* isServer */)
return config
}
@@ -24,7 +24,7 @@ module.exports = function createServerConfig (ctx) {
config
.entry('app')
.add(path.resolve(__dirname, '../app/serverEntry.js'))
.add(ctx.getLibFilePath('client/serverEntry.js'))
config.output
.filename('server-bundle.js')
@@ -56,7 +56,7 @@ module.exports = function createServerConfig (ctx) {
}])
}
ctx.pluginAPI.options.chainWebpack.syncApply(config, true /* isServer */)
ctx.pluginAPI.applySyncOption('chainWebpack', config, true /* isServer */)
return config
}
@@ -1,19 +0,0 @@
'use strict'
/**
* Module dependencies.
*/
const AppContext = require('./AppContext')
const { logger } = require('@vuepress/shared-utils')
/**
* Expose prepare.
*/
module.exports = async function prepare (sourceDir, cliOptions, isProd) {
logger.wait('Extracting site metadata...')
const appContext = AppContext.getInstance(sourceDir, cliOptions, isProd)
await appContext.process()
return appContext
}
@@ -1,35 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`containers danger 1`] = `
<div class="danger custom-block">
<p class="custom-block-title">WARNING</p>
<p>I am a danger</p>
</div>
`;
exports[`containers tip 1`] = `
<div class="tip custom-block">
<p class="custom-block-title">TIP</p>
<p>I am a tip</p>
</div>
`;
exports[`containers tip-override 1`] = `
<div class="tip custom-block">
<p class="custom-block-title">提示</p>
<p>I am a tip</p>
</div>
`;
exports[`containers v-pre 1`] = `
<div v-pre>
<p>I am a v-pre</p>
</div>
`;
exports[`containers warning 1`] = `
<div class="warning custom-block">
<p class="custom-block-title">WARNING</p>
<p>I am a warning</p>
</div>
`;
@@ -3,18 +3,19 @@
exports[`highlightLines highlight multiple lines 1`] = `
<div class="highlight-lines">
<div class="highlighted">&nbsp;</div>
<div class="highlighted">&nbsp;</div><br>
<div class="highlighted">&nbsp;</div>
<br>
<div class="highlighted">&nbsp;</div>
<div class="highlighted">&nbsp;</div>
<br>
<br>
</div>const app = new Vue({ render, router }) app.$mount('#app')
<div class="highlighted">&nbsp;</div><br><br>
</div>const app = new Vue({
render,
router
})
app.$mount('#app')
`;
exports[`highlightLines highlight single line 1`] = `
<div class="highlight-lines">
<div class="highlighted">&nbsp;</div>
<br>
<div class="highlighted">&nbsp;</div><br>
</div>new Vue()
`;
@@ -18,6 +18,7 @@ exports[`hoist Should miss script and style when using hoist 1`] = `
exports[`hoist Should miss script and style when using hoist 2`] = `
Object {
"__data_block": Object {},
"hoistedTags": Array [
<script src="vue.js"></script>,
<style>
@@ -5,8 +5,7 @@ exports[`lineNumbers should lineNumbers work with highlightLines 1`] = `
<div class="language-js line-numbers-mode">
<!--afterbegin-->
<div class="highlight-lines">
<div class="highlighted">&nbsp;</div>
<br>
<div class="highlighted">&nbsp;</div><br>
</div>new Vue()
<div class="line-numbers-wrapper"></div>
<!--beforeend-->
@@ -1,25 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`link should render external links correctly 1`] = `
<p>
<a href="https://vuejs.org/" target="_blank" rel="noopener noreferrer">vue
<OutboundLink/>
</a>
</p>
<p><a href="https://vuejs.org/" target="_blank" rel="noopener noreferrer">vue
<OutboundLink /></a></p>
`;
exports[`link should render external links correctly 2`] = `
<p>
<a href="http://vuejs.org/" target="_blank" rel="noopener noreferrer">vue
<OutboundLink/>
</a>
</p>
<p><a href="http://vuejs.org/" target="_blank" rel="noopener noreferrer">vue
<OutboundLink /></a></p>
`;
exports[`link should render external links correctly 3`] = `
<p>
<a href="https://google.com" target="_blank" rel="noopener noreferrer">some <strong>link</strong> with <code>code</code>
<OutboundLink/>
</a>
</p>
<p><a href="https://google.com" target="_blank" rel="noopener noreferrer">some <strong>link</strong> with <code>code</code>
<OutboundLink /></a></p>
`;

Some files were not shown because too many files have changed in this diff Show More