Files
vuepress/lib/prepare.js
T
2018-04-18 16:51:47 -04:00

286 lines
8.2 KiB
JavaScript

const path = require('path')
const fs = require('fs-extra')
const globby = require('globby')
const yamlParser = require('js-yaml')
const yaml = require('yaml-front-matter')
const createMarkdown = require('./markdown')
const tempPath = path.resolve(__dirname, 'app/.temp')
const { inferTitle, extractHeaders } = require('./util')
fs.ensureDirSync(tempPath)
const tempCache = new Map()
async function writeTemp (file, content) {
// cache write to avoid hitting the dist if it didn't change
const cached = tempCache.get(file)
if (cached !== content) {
await fs.writeFile(path.join(tempPath, file), content)
tempCache.set(file, content)
}
}
module.exports = async function prepare (sourceDir) {
// 1. load options
const options = await resolveOptions(sourceDir)
// 2. generate routes & user components registration code
const routesCode = await genRoutesFile(options)
const componentCode = await genComponentRegistrationFile(options)
await writeTemp('routes.js', [
componentCode,
routesCode
].join('\n\n'))
// 3. generate siteData
const dataCode = `export const siteData = ${JSON.stringify(options.siteData, null, 2)}`
await writeTemp('siteData.js', dataCode)
// 4. generate basic polyfill if need to support older browsers
let polyfillCode = ``
if (!options.siteConfig.evergreen) {
polyfillCode =
`import 'es6-promise/auto'
if (!Object.assign) Object.assign = require('object-assign')`
}
await writeTemp('polyfill.js', polyfillCode)
// 5. handle user override
if (options.useDefaultTheme) {
const overridePath = path.resolve(sourceDir, '.vuepress/override.styl')
const hasUserOverride = fs.existsSync(overridePath)
await writeTemp(`override.styl`, hasUserOverride ? `@import(${JSON.stringify(overridePath)})` : ``)
}
// 6. handle enhanceApp.js
const enhancePath = path.resolve(sourceDir, '.vuepress/enhanceApp.js')
const hasEnhancePath = fs.existsSync(enhancePath)
await writeTemp(
'enhanceApp.js',
hasEnhancePath
? `export { default } from ${JSON.stringify(enhancePath)}`
: `export default function () {}`
)
return options
}
async function resolveOptions (sourceDir) {
const vuepressDir = path.resolve(sourceDir, '.vuepress')
const configPath = path.resolve(vuepressDir, 'config.js')
const configYmlPath = path.resolve(vuepressDir, 'config.yml')
delete require.cache[configPath]
let siteConfig = {}
if (fs.existsSync(configYmlPath)) {
const content = await fs.readFile(configYmlPath, 'utf-8')
siteConfig = yamlParser.safeLoad(content)
} else if (fs.existsSync(configPath)) {
siteConfig = require(configPath)
}
// 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 theme
const useDefaultTheme = (
!siteConfig.theme &&
!fs.existsSync(path.resolve(vuepressDir, 'theme'))
)
const options = {
siteConfig,
sourceDir,
outDir: siteConfig.dest
? path.resolve(siteConfig.dest)
: path.resolve(sourceDir, '.vuepress/dist'),
publicPath: base,
pageFiles: sort(await globby(['**/*.md', '!.vuepress', '!node_modules'], { cwd: sourceDir })),
pagesData: null,
themePath: null,
notFoundPath: null,
useDefaultTheme,
markdown: createMarkdown(siteConfig)
}
if (useDefaultTheme) {
// use default theme
options.themePath = path.resolve(__dirname, 'default-theme/Layout.vue')
options.notFoundPath = path.resolve(__dirname, 'default-theme/NotFound.vue')
} else {
let themeDir
let themePath
// resolve custom theme
if (siteConfig.theme) {
try {
themePath = require.resolve(`vuepress-theme-${siteConfig.theme}/Layout.vue`)
themeDir = path.dirname(themePath)
} catch (e) {
throw new Error(`[vuepress] Failed to load custom theme "${
siteConfig.theme
}". File vuepress-theme-${siteConfig.theme}/Layout.vue does not exist.`)
}
} else {
themeDir = path.resolve(vuepressDir, 'theme')
themePath = path.resolve(themeDir, 'Layout.vue')
if (!fs.existsSync(themePath)) {
throw new Error(`[vuepress] Cannot resolve Layout.vue file in .vuepress/theme.`)
}
}
options.themePath = themePath
const notFoundPath = path.resolve(themeDir, 'NotFound.vue')
if (fs.existsSync(notFoundPath)) {
options.notFoundPath = notFoundPath
} else {
options.notFoundPath = path.resolve(__dirname, 'default-theme/NotFound.vue')
}
}
// resolve pages
const pagesData = await Promise.all(options.pageFiles.map(async (file) => {
const data = {
path: fileToPath(file)
}
// extract yaml frontmatter
const content = await fs.readFile(path.resolve(sourceDir, file), 'utf-8')
const frontmatter = yaml.loadFront(content)
// infer title
const title = inferTitle(frontmatter)
if (title) {
data.title = title
}
const headers = extractHeaders(
frontmatter.__content,
['h2', 'h3'],
options.markdown
)
if (headers.length) {
data.headers = headers
}
delete frontmatter.__content
if (Object.keys(frontmatter).length) {
data.frontmatter = frontmatter
}
return data
}))
// resolve site data
options.siteData = {
title: siteConfig.title || '',
description: siteConfig.description || '',
base: siteConfig.base || '/',
pages: pagesData,
themeConfig: siteConfig.themeConfig || {},
locales: siteConfig.locales
}
return options
}
async function genComponentRegistrationFile ({ sourceDir }) {
function genImport (file) {
const name = fileToComponentName(file)
const baseDir = path.resolve(sourceDir, '.vuepress/components')
const absolutePath = path.resolve(baseDir, file)
const code = `Vue.component(${JSON.stringify(name)}, () => import(${JSON.stringify(absolutePath)}))`
return code
}
const components = (await resolveComponents(sourceDir)) || []
return `import Vue from 'vue'\n` + components.map(genImport).join('\n')
}
const indexRE = /\b(index|readme)\.md$/i
const extRE = /\.(vue|md)$/
function fileToPath (file) {
if (isIndexFile(file)) {
// README.md -> /
// foo/README.md -> /foo/
return '/' + file.replace(indexRE, '')
} else {
// foo.md -> /foo.html
// foo/bar.md -> /foo/bar.html
return `/${file.replace(extRE, '').replace(/\\/g, '/')}.html`
}
}
function fileToComponentName (file) {
let normalizedName = file
.replace(/\/|\\/g, '-')
.replace(extRE, '')
if (isIndexFile(file)) {
normalizedName = normalizedName.replace(/readme$/i, 'index')
}
const pagePrefix = /\.md$/.test(file) ? `page-` : ``
return `${pagePrefix}${normalizedName}`
}
function isIndexFile (file) {
return indexRE.test(file)
}
async function resolveComponents (sourceDir) {
const componentDir = path.resolve(sourceDir, '.vuepress/components')
if (!fs.existsSync(componentDir)) {
return
}
return sort(await globby(['**/*.vue'], { cwd: componentDir }))
}
async function genRoutesFile ({ siteData: { pages }, sourceDir, pageFiles }) {
function genRoute ({ path: pagePath }, index) {
const file = pageFiles[index]
const filePath = path.resolve(sourceDir, file)
let code = `
{
path: ${JSON.stringify(pagePath)},
component: Theme,
beforeEnter: (to, from, next) => {
import(${JSON.stringify(filePath)}).then(comp => {
Vue.component(${JSON.stringify(fileToComponentName(file))}, comp.default)
next()
})
}
}`
if (/\/$/.test(pagePath)) {
code += `,{
path: ${JSON.stringify(pagePath + 'index.html')},
redirect: ${JSON.stringify(pagePath)}
}`
}
return code
}
return (
`import Theme from '@theme'\n` +
`export const routes = [${pages.map(genRoute).join(',')}\n]`
)
}
function sort (arr) {
return arr.sort((a, b) => {
if (a < b) return -1
if (a > b) return 1
return 0
})
}