未验证 提交 cd8ee42f 编写于 作者: U ULIVZ 提交者: GitHub

refactor: structured and self-contained prepare (#531)

上级 f9c39140
const path = require('path')
const { fileToComponentName, resolveComponents } = require('./util')
exports.genRoutesFile = async function ({
siteData: { pages },
sourceDir,
pageFiles
}) {
function genRoute ({ path: pagePath, key: componentName }, index) {
const file = pageFiles[index]
const filePath = path.resolve(sourceDir, file)
let code = `
{
name: ${JSON.stringify(componentName)},
path: ${JSON.stringify(pagePath)},
component: ThemeLayout,
beforeEnter: (to, from, next) => {
import(${JSON.stringify(filePath)}).then(comp => {
Vue.component(${JSON.stringify(componentName)}, comp.default)
next()
})
}
}`
const dncodedPath = decodeURIComponent(pagePath)
if (dncodedPath !== pagePath) {
code += `,
{
path: ${JSON.stringify(dncodedPath)},
redirect: ${JSON.stringify(pagePath)}
}`
}
if (/\/$/.test(pagePath)) {
code += `,
{
path: ${JSON.stringify(pagePath + 'index.html')},
redirect: ${JSON.stringify(pagePath)}
}`
}
return code
}
const notFoundRoute = `,
{
path: '*',
component: ThemeNotFound
}`
return (
`import ThemeLayout from '@themeLayout'\n` +
`import ThemeNotFound from '@themeNotFound'\n` +
`import { injectMixins } from '@app/util'\n` +
`import rootMixins from '@app/root-mixins'\n\n` +
`injectMixins(ThemeLayout, rootMixins)\n` +
`injectMixins(ThemeNotFound, rootMixins)\n\n` +
`export const routes = [${pages.map(genRoute).join(',')}${notFoundRoute}\n]`
)
}
exports.genComponentRegistrationFile = async function ({ 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 path = require('path')
const fs = require('fs-extra')
const resolveOptions = require('./resolveOptions')
const { genRoutesFile, genComponentRegistrationFile } = require('./codegen')
const { writeTemp, writeEnhanceTemp } = require('./util')
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'))
// 3. generate siteData
const dataCode = `export const siteData = ${JSON.stringify(options.siteData, null, 2)}`
await writeTemp('siteData.js', dataCode)
// 4. handle user override
const overridePath = path.resolve(sourceDir, '.vuepress/override.styl')
const hasUserOverride = fs.existsSync(overridePath)
await writeTemp(`override.styl`, hasUserOverride ? `@import(${JSON.stringify(overridePath)})` : ``)
// 5. handle enhanceApp.js
const enhanceAppPath = path.resolve(sourceDir, '.vuepress/enhanceApp.js')
await writeEnhanceTemp('enhanceApp.js', enhanceAppPath)
// 6. handle the theme enhanceApp.js
await writeEnhanceTemp('themeEnhanceApp.js', options.themeEnhanceAppPath)
return options
}
const path = require('path')
const fs = require('fs-extra') const fs = require('fs-extra')
const path = require('path')
const globby = require('globby') const globby = require('globby')
const createMarkdown = require('./markdown') const createMarkdown = require('../markdown')
const loadConfig = require('./util/loadConfig') const loadConfig = require('./loadConfig')
const tempPath = path.resolve(__dirname, 'app/.temp') const { encodePath, fileToPath, sort, getGitLastUpdatedTimeStamp } = require('./util')
const { const {
encodePath,
inferTitle, inferTitle,
extractHeaders, extractHeaders,
parseFrontmatter, parseFrontmatter
getGitLastUpdatedTimeStamp } = require('../util/index')
} = 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)
}
}
async function writeEnhanceTemp (destName, srcPath) {
await writeTemp(
destName,
fs.existsSync(srcPath)
? `export { default } from ${JSON.stringify(srcPath)}`
: `export default function () {}`
)
}
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'))
// 3. generate siteData
const dataCode = `export const siteData = ${JSON.stringify(options.siteData, null, 2)}`
await writeTemp('siteData.js', dataCode)
// 4. handle user override module.exports = async function resolveOptions (sourceDir) {
const overridePath = path.resolve(sourceDir, '.vuepress/override.styl')
const hasUserOverride = fs.existsSync(overridePath)
await writeTemp(`override.styl`, hasUserOverride ? `@import(${JSON.stringify(overridePath)})` : ``)
// 5. handle enhanceApp.js
const enhanceAppPath = path.resolve(sourceDir, '.vuepress/enhanceApp.js')
await writeEnhanceTemp('enhanceApp.js', enhanceAppPath)
// 6. handle the theme enhanceApp.js
await writeEnhanceTemp('themeEnhanceApp.js', options.themeEnhanceAppPath)
return options
}
async function resolveOptions (sourceDir) {
const vuepressDir = path.resolve(sourceDir, '.vuepress') const vuepressDir = path.resolve(sourceDir, '.vuepress')
const siteConfig = loadConfig(vuepressDir) const siteConfig = loadConfig(vuepressDir)
...@@ -97,7 +42,7 @@ async function resolveOptions (sourceDir) { ...@@ -97,7 +42,7 @@ async function resolveOptions (sourceDir) {
!siteConfig.theme && !siteConfig.theme &&
!fs.existsSync(path.resolve(vuepressDir, 'theme')) !fs.existsSync(path.resolve(vuepressDir, 'theme'))
) )
const defaultThemePath = path.resolve(__dirname, 'default-theme') const defaultThemePath = path.resolve(__dirname, '../default-theme')
let themePath = null let themePath = null
let themeLayoutPath = null let themeLayoutPath = null
let themeNotFoundPath = null let themeNotFoundPath = null
...@@ -115,7 +60,7 @@ async function resolveOptions (sourceDir) { ...@@ -115,7 +60,7 @@ async function resolveOptions (sourceDir) {
try { try {
themeLayoutPath = require.resolve(`vuepress-theme-${siteConfig.theme}/Layout.vue`, { themeLayoutPath = require.resolve(`vuepress-theme-${siteConfig.theme}/Layout.vue`, {
paths: [ paths: [
path.resolve(__dirname, '../node_modules'), path.resolve(__dirname, '../../node_modules'),
path.resolve(sourceDir) path.resolve(sourceDir)
] ]
}) })
...@@ -154,7 +99,7 @@ async function resolveOptions (sourceDir) { ...@@ -154,7 +99,7 @@ async function resolveOptions (sourceDir) {
const isAlgoliaSearch = ( const isAlgoliaSearch = (
themeConfig.algolia || themeConfig.algolia ||
Object.keys(siteConfig.locales && themeConfig.locales || {}) Object.keys(siteConfig.locales && themeConfig.locales || {})
.some(base => themeConfig.locales[base].algolia) .some(base => themeConfig.locales[base].algolia)
) )
// resolve markdown // resolve markdown
...@@ -167,7 +112,7 @@ async function resolveOptions (sourceDir) { ...@@ -167,7 +112,7 @@ async function resolveOptions (sourceDir) {
const shouldResolveLastUpdated = ( const shouldResolveLastUpdated = (
themeConfig.lastUpdated || themeConfig.lastUpdated ||
Object.keys(siteConfig.locales && themeConfig.locales || {}) Object.keys(siteConfig.locales && themeConfig.locales || {})
.some(base => themeConfig.locales[base].lastUpdated) .some(base => themeConfig.locales[base].lastUpdated)
) )
// resolve pagesData // resolve pagesData
...@@ -238,115 +183,3 @@ async function resolveOptions (sourceDir) { ...@@ -238,115 +183,3 @@ async function resolveOptions (sourceDir) {
return options 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 = /(^|.*\/)(index|readme)\.md$/i
const extRE = /\.(vue|md)$/
function fileToPath (file) {
if (isIndexFile(file)) {
// README.md -> /
// foo/README.md -> /foo/
return file.replace(indexRE, '/$1')
} 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, key: componentName }, index) {
const file = pageFiles[index]
const filePath = path.resolve(sourceDir, file)
let code = `
{
name: ${JSON.stringify(componentName)},
path: ${JSON.stringify(pagePath)},
component: ThemeLayout,
beforeEnter: (to, from, next) => {
import(${JSON.stringify(filePath)}).then(comp => {
Vue.component(${JSON.stringify(componentName)}, comp.default)
next()
})
}
}`
const dncodedPath = decodeURIComponent(pagePath)
if (dncodedPath !== pagePath) {
code += `,
{
path: ${JSON.stringify(dncodedPath)},
redirect: ${JSON.stringify(pagePath)}
}`
}
if (/\/$/.test(pagePath)) {
code += `,
{
path: ${JSON.stringify(pagePath + 'index.html')},
redirect: ${JSON.stringify(pagePath)}
}`
}
return code
}
const notFoundRoute = `,
{
path: '*',
component: ThemeNotFound
}`
return (
`import ThemeLayout from '@themeLayout'\n` +
`import ThemeNotFound from '@themeNotFound'\n` +
`import { injectMixins } from '@app/util'\n` +
`import rootMixins from '@app/root-mixins'\n\n` +
`injectMixins(ThemeLayout, rootMixins)\n` +
`injectMixins(ThemeNotFound, rootMixins)\n\n` +
`export const routes = [${pages.map(genRoute).join(',')}${notFoundRoute}\n]`
)
}
function sort (arr) {
return arr.sort((a, b) => {
if (a < b) return -1
if (a > b) return 1
return 0
})
}
const path = require('path')
const spawn = require('cross-spawn')
const fs = require('fs-extra')
const globby = require('globby')
const tempPath = path.resolve(__dirname, '../app/.temp')
fs.ensureDirSync(tempPath)
const tempCache = new Map()
exports.writeTemp = async function (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)
}
}
exports.writeEnhanceTemp = async function (destName, srcPath) {
await exports.writeTemp(
destName,
fs.existsSync(srcPath)
? `export { default } from ${JSON.stringify(srcPath)}`
: `export default function () {}`
)
}
const indexRE = /(^|.*\/)(index|readme)\.md$/i
const extRE = /\.(vue|md)$/
exports.fileToPath = function (file) {
if (exports.isIndexFile(file)) {
// README.md -> /
// foo/README.md -> /foo/
return file.replace(indexRE, '/$1')
} else {
// foo.md -> /foo.html
// foo/bar.md -> /foo/bar.html
return `/${file.replace(extRE, '').replace(/\\/g, '/')}.html`
}
}
exports.fileToComponentName = function (file) {
let normalizedName = file
.replace(/\/|\\/g, '-')
.replace(extRE, '')
if (exports.isIndexFile(file)) {
normalizedName = normalizedName.replace(/readme$/i, 'index')
}
const pagePrefix = /\.md$/.test(file) ? `page-` : ``
return `${pagePrefix}${normalizedName}`
}
exports.isIndexFile = function (file) {
return indexRE.test(file)
}
exports.resolveComponents = async function (sourceDir) {
const componentDir = path.resolve(sourceDir, '.vuepress/components')
if (!fs.existsSync(componentDir)) {
return
}
return exports.sort(await globby(['**/*.vue'], { cwd: componentDir }))
}
exports.sort = function (arr) {
return arr.sort((a, b) => {
if (a < b) return -1
if (a > b) return 1
return 0
})
}
exports.encodePath = function (userpath) {
return userpath.split('/').map(item => encodeURIComponent(item)).join('/')
}
exports.getGitLastUpdatedTimeStamp = function (filepath) {
return parseInt(spawn.sync('git', ['log', '-1', '--format=%ct', filepath]).stdout.toString('utf-8')) * 1000
}
const spawn = require('cross-spawn')
const parseHeaders = require('./parseHeaders') const parseHeaders = require('./parseHeaders')
exports.encodePath = path => { exports.normalizeHeadTag = function (tag) {
return path.split('/').map(item => encodeURIComponent(item)).join('/')
}
exports.normalizeHeadTag = tag => {
if (typeof tag === 'string') { if (typeof tag === 'string') {
tag = [tag] tag = [tag]
} }
...@@ -18,7 +13,7 @@ exports.normalizeHeadTag = tag => { ...@@ -18,7 +13,7 @@ exports.normalizeHeadTag = tag => {
} }
} }
exports.applyUserWebpackConfig = (userConfig, config, isServer) => { exports.applyUserWebpackConfig = function (userConfig, config, isServer) {
const merge = require('webpack-merge') const merge = require('webpack-merge')
if (typeof userConfig === 'object') { if (typeof userConfig === 'object') {
return merge(config, userConfig) return merge(config, userConfig)
...@@ -32,7 +27,7 @@ exports.applyUserWebpackConfig = (userConfig, config, isServer) => { ...@@ -32,7 +27,7 @@ exports.applyUserWebpackConfig = (userConfig, config, isServer) => {
return config return config
} }
exports.inferTitle = frontmatter => { exports.inferTitle = function (frontmatter) {
if (frontmatter.data.home) { if (frontmatter.data.home) {
return 'Home' return 'Home'
} }
...@@ -45,7 +40,7 @@ exports.inferTitle = frontmatter => { ...@@ -45,7 +40,7 @@ exports.inferTitle = frontmatter => {
} }
} }
exports.parseFrontmatter = content => { exports.parseFrontmatter = function (content) {
const matter = require('gray-matter') const matter = require('gray-matter')
const toml = require('toml') const toml = require('toml')
...@@ -61,7 +56,7 @@ exports.parseFrontmatter = content => { ...@@ -61,7 +56,7 @@ exports.parseFrontmatter = content => {
const LRU = require('lru-cache') const LRU = require('lru-cache')
const cache = LRU({ max: 1000 }) const cache = LRU({ max: 1000 })
exports.extractHeaders = (content, include = [], md) => { exports.extractHeaders = function (content, include = [], md) {
const key = content + include.join(',') const key = content + include.join(',')
const hit = cache.get(key) const hit = cache.get(key)
if (hit) { if (hit) {
...@@ -86,7 +81,3 @@ exports.extractHeaders = (content, include = [], md) => { ...@@ -86,7 +81,3 @@ exports.extractHeaders = (content, include = [], md) => {
cache.set(key, res) cache.set(key, res)
return res return res
} }
exports.getGitLastUpdatedTimeStamp = filepath => {
return parseInt(spawn.sync('git', ['log', '-1', '--format=%ct', filepath]).stdout.toString('utf-8')) * 1000
}
module.exports = {
title: 'Hello VuePress',
description: 'Just playing around'
}
module.exports = {
title: 'Hello VuePress',
description: 'Just playing around',
dest: 'vuepress',
base: 'vuepress',
head: [
['link', { rel: 'icon', href: '/logo.png' }]
]
}
import path from 'path'
import resolveOptions from '@/prepare/resolveOptions.js'
const DEFAULT_THEME_PATH = path.resolve(__dirname, '../../lib/default-theme')
const DEFAULT_THEME_LAYOUT_PATH = path.resolve(DEFAULT_THEME_PATH, 'Layout.vue')
const DEFAULT_THEME_NOT_FOOUND_PATH = path.resolve(DEFAULT_THEME_PATH, 'NotFound.vue')
describe('prepare - resolveOptions', () => {
test('single file docs without config.', async () => {
const { docsDir } = getDocsPaths('docs-simple')
const options = await resolveOptions(docsDir)
const {
siteConfig,
siteData,
sourceDir,
outDir,
publicPath,
pageFiles,
pagesData,
themePath,
themeLayoutPath,
themeNotFoundPath,
themeEnhanceAppPath,
useDefaultTheme,
isAlgoliaSearch,
markdown
} = options
expect(siteConfig).toEqual({})
expect(siteData).toEqual({
title: '',
description: '',
base: '/',
pages: pagesData,
themeConfig: {},
locales: undefined
})
expect(sourceDir).toBe(docsDir)
expect(outDir).toBe(path.resolve(docsDir, '.vuepress/dist'))
expect(publicPath).toBe('/')
expect(pageFiles).toHaveLength(1)
expect(pageFiles[0]).toBe('README.md')
expect(themePath).toBe(DEFAULT_THEME_PATH)
expect(themeLayoutPath).toBe(DEFAULT_THEME_LAYOUT_PATH)
expect(themeNotFoundPath).toBe(DEFAULT_THEME_NOT_FOOUND_PATH)
expect(themeEnhanceAppPath).toBe(null)
expect(useDefaultTheme).toBe(true)
expect(isAlgoliaSearch).toBe(false)
expect(typeof markdown).toBe('object')
})
test('single file docs with config', async () => {
const { docsDir, configPath } = getDocsPaths('docs-simple-config')
const options = await resolveOptions(docsDir)
const {
siteConfig,
outDir,
publicPath
} = options
expect(siteConfig).toEqual(require(configPath))
expect(siteConfig.base).toBe('vuepress')
expect(siteConfig.dest).toBe('vuepress')
expect(outDir).toBe(path.resolve('vuepress'))
expect(publicPath).toBe('vuepress')
})
test('simple docs with custom theme', async () => {
const paths = getDocsPaths('docs-custom-theme')
const options = await resolveOptions(paths.docsDir)
const {
themePath,
themeLayoutPath,
themeNotFoundPath,
useDefaultTheme
} = options
expect(useDefaultTheme).toBe(false)
expect(themePath).toBe(paths.themePath)
expect(themeLayoutPath).toBe(paths.themeLayoutPath)
expect(themeNotFoundPath).toBe(DEFAULT_THEME_NOT_FOOUND_PATH) // fallbacks to default theme's NotFound component.
})
})
function getDocsPaths (name) {
const docsDir = path.join(__dirname, `fixtures/${name}`)
const configPath = path.join(docsDir, '.vuepress/config.js')
const themePath = path.join(docsDir, '.vuepress/theme')
const themeLayoutPath = path.join(themePath, 'Layout.vue')
const themeNotFoundPath = path.join(themePath, 'NotFound.vue')
return {
docsDir,
configPath,
themePath,
themeLayoutPath,
themeNotFoundPath
}
}
import {
isIndexFile,
fileToPath
} from '@/prepare/util.js'
describe('prepare - util', () => {
test('isIndexFile', () => {
[
'README.md',
'readme.md',
'INDEX.md',
'index.md',
'foo/README.md',
'foo/index.md'
].forEach(file => {
expect(isIndexFile(file)).toBe(true)
});
[
'foo/one.md',
'one.md'
].forEach(file => {
expect(isIndexFile(file)).toBe(false)
})
})
test('fileToPath', () => {
const asserts = {
'README.md': '/',
'foo/README.md': '/foo/',
'foo.md': '/foo.html',
'foo/bar.md': '/foo/bar.html'
}
Object.keys(asserts).forEach(file => {
expect(fileToPath(file)).toBe(asserts[file])
})
})
})
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册