prepare.js 9.3 KB
Newer Older
E
Evan You 已提交
1
const path = require('path')
2
const fs = require('fs-extra')
E
Evan You 已提交
3
const globby = require('globby')
A
Aliex Leung 已提交
4
const yamlParser = require('js-yaml')
5
const tomlParser = require('toml')
6
const createMarkdown = require('./markdown')
E
Evan You 已提交
7
const tempPath = path.resolve(__dirname, 'app/.temp')
8
const { inferTitle, extractHeaders, parseFrontmatter } = require('./util')
E
Evan You 已提交
9

E
Evan You 已提交
10
fs.ensureDirSync(tempPath)
E
Evan You 已提交
11

E
Evan You 已提交
12
const tempCache = new Map()
E
Evan You 已提交
13
async function writeTemp (file, content) {
E
Evan You 已提交
14 15 16
  // cache write to avoid hitting the dist if it didn't change
  const cached = tempCache.get(file)
  if (cached !== content) {
E
Evan You 已提交
17
    await fs.writeFile(path.join(tempPath, file), content)
E
Evan You 已提交
18 19 20 21
    tempCache.set(file, content)
  }
}

E
Evan You 已提交
22 23 24 25
module.exports = async function prepare (sourceDir) {
  // 1. load options
  const options = await resolveOptions(sourceDir)

E
Evan You 已提交
26 27
  // 2. generate routes & user components registration code
  const routesCode = await genRoutesFile(options)
E
tweaks  
Evan You 已提交
28
  const componentCode = await genComponentRegistrationFile(options)
E
Evan You 已提交
29

E
Evan You 已提交
30
  await writeTemp('routes.js', [
E
Evan You 已提交
31 32 33
    componentCode,
    routesCode
  ].join('\n\n'))
E
tweaks  
Evan You 已提交
34

E
Evan You 已提交
35
  // 3. generate siteData
E
tweaks  
Evan You 已提交
36
  const dataCode = `export const siteData = ${JSON.stringify(options.siteData, null, 2)}`
E
Evan You 已提交
37
  await writeTemp('siteData.js', dataCode)
E
tweaks  
Evan You 已提交
38

E
Evan You 已提交
39
  // 4. generate basic polyfill if need to support older browsers
40 41 42 43 44 45
  let polyfillCode = ``
  if (!options.siteConfig.evergreen) {
    polyfillCode =
`import 'es6-promise/auto'
if (!Object.assign) Object.assign = require('object-assign')`
  }
E
Evan You 已提交
46
  await writeTemp('polyfill.js', polyfillCode)
47

E
Evan You 已提交
48
  // 5. handle user override
E
Evan You 已提交
49
  if (options.useDefaultTheme) {
E
Evan You 已提交
50 51
    const overridePath = path.resolve(sourceDir, '.vuepress/override.styl')
    const hasUserOverride = fs.existsSync(overridePath)
E
Evan You 已提交
52
    await writeTemp(`override.styl`, hasUserOverride ? `@import(${JSON.stringify(overridePath)})` : ``)
E
Evan You 已提交
53 54
  }

55 56 57 58 59 60 61 62 63
  async function writeEnhanceTemp (destName, srcPath, isEnhanceExist) {
    await writeTemp(
      destName,
      isEnhanceExist
        ? `export { default } from ${JSON.stringify(srcPath)}`
        : `export default function () {}`
    )
  }

方剑成 已提交
64
  // 6. handle enhanceApp.js
65 66 67 68 69
  const enhanceAppPath = path.resolve(sourceDir, '.vuepress/enhanceApp.js')
  writeEnhanceTemp('enhanceApp.js', enhanceAppPath, fs.existsSync(enhanceAppPath))

  // 7. handle the theme index.js
  writeEnhanceTemp('themeEnhanceApp.js', options.themeApp, fs.existsSync(options.themeApp))
方剑成 已提交
70

E
Evan You 已提交
71 72 73 74
  return options
}

async function resolveOptions (sourceDir) {
E
Evan You 已提交
75 76
  const vuepressDir = path.resolve(sourceDir, '.vuepress')
  const configPath = path.resolve(vuepressDir, 'config.js')
A
Aliex Leung 已提交
77
  const configYmlPath = path.resolve(vuepressDir, 'config.yml')
78
  const configTomlPath = path.resolve(vuepressDir, 'config.toml')
79 80

  delete require.cache[configPath]
A
Aliex Leung 已提交
81 82
  let siteConfig = {}
  if (fs.existsSync(configYmlPath)) {
83 84 85
    siteConfig = await parseConfig(configYmlPath)
  } else if (fs.existsSync(configTomlPath)) {
    siteConfig = await parseConfig(configTomlPath)
A
Aliex Leung 已提交
86 87 88
  } else if (fs.existsSync(configPath)) {
    siteConfig = require(configPath)
  }
E
Evan You 已提交
89

E
Evan You 已提交
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
  // 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)
            }
          }
        }
      }
    })
  }

E
Evan You 已提交
108 109 110 111 112 113
  // resolve theme
  const useDefaultTheme = (
    !siteConfig.theme &&
    !fs.existsSync(path.resolve(vuepressDir, 'theme'))
  )

E
Evan You 已提交
114
  const options = {
E
Evan You 已提交
115
    siteConfig,
E
Evan You 已提交
116
    sourceDir,
E
Evan You 已提交
117 118 119
    outDir: siteConfig.dest
      ? path.resolve(siteConfig.dest)
      : path.resolve(sourceDir, '.vuepress/dist'),
E
Evan You 已提交
120
    publicPath: base,
121
    pageFiles: sort(await globby(['**/*.md', '!.vuepress', '!node_modules'], { cwd: sourceDir })),
E
Evan You 已提交
122 123
    pagesData: null,
    themePath: null,
E
Evan You 已提交
124
    notFoundPath: null,
125 126
    useDefaultTheme,
    markdown: createMarkdown(siteConfig)
E
Evan You 已提交
127 128
  }

E
Evan You 已提交
129
  if (useDefaultTheme) {
E
Evan You 已提交
130 131 132 133
    // use default theme
    options.themePath = path.resolve(__dirname, 'default-theme/Layout.vue')
    options.notFoundPath = path.resolve(__dirname, 'default-theme/NotFound.vue')
  } else {
E
Evan You 已提交
134 135
    let themeDir
    let themePath
E
Evan You 已提交
136
    // resolve custom theme
E
Evan You 已提交
137 138 139 140 141 142 143 144 145
    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.`)
      }
E
Evan You 已提交
146
    } else {
E
Evan You 已提交
147 148 149 150 151
      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.`)
      }
E
Evan You 已提交
152
    }
E
Evan You 已提交
153
    options.themePath = themePath
E
Evan You 已提交
154

155
    const notFoundPath = path.resolve(themeDir, 'NotFound.vue')
E
Evan You 已提交
156 157 158
    if (fs.existsSync(notFoundPath)) {
      options.notFoundPath = notFoundPath
    } else {
E
Evan You 已提交
159
      options.notFoundPath = path.resolve(__dirname, 'default-theme/NotFound.vue')
E
Evan You 已提交
160
    }
161 162 163 164 165

    const themeApp = path.resolve(themeDir, 'index.js')
    if (fs.existsSync(themeApp)) {
      options.themeApp = themeApp
    }
E
tweaks  
Evan You 已提交
166 167
  }

E
Evan You 已提交
168
  // resolve pages
E
Evan You 已提交
169
  const pagesData = await Promise.all(options.pageFiles.map(async (file) => {
E
tweaks  
Evan You 已提交
170
    const data = {
E
Evan You 已提交
171
      path: fileToPath(file)
E
Evan You 已提交
172
    }
E
Evan You 已提交
173 174

    // extract yaml frontmatter
E
Evan You 已提交
175
    const content = await fs.readFile(path.resolve(sourceDir, file), 'utf-8')
176
    const frontmatter = parseFrontmatter(content)
E
Evan You 已提交
177
    // infer title
E
Evan You 已提交
178 179 180
    const title = inferTitle(frontmatter)
    if (title) {
      data.title = title
E
Evan You 已提交
181
    }
182
    const headers = extractHeaders(
183
      frontmatter.content,
184 185 186
      ['h2', 'h3'],
      options.markdown
    )
E
Evan You 已提交
187 188 189
    if (headers.length) {
      data.headers = headers
    }
190 191
    if (Object.keys(frontmatter.data).length) {
      data.frontmatter = frontmatter.data
E
Evan You 已提交
192
    }
193 194 195
    if (frontmatter.excerpt) {
      data.excerpt = frontmatter.excerpt
    }
E
tweaks  
Evan You 已提交
196
    return data
E
Evan You 已提交
197
  }))
E
Evan You 已提交
198

E
Evan You 已提交
199
  // resolve site data
E
Evan You 已提交
200
  options.siteData = {
E
Evan You 已提交
201 202
    title: siteConfig.title || '',
    description: siteConfig.description || '',
E
Evan You 已提交
203
    base: siteConfig.base || '/',
E
Evan You 已提交
204
    pages: pagesData,
205
    themeConfig: siteConfig.themeConfig || {},
E
Evan You 已提交
206
    locales: siteConfig.locales
E
Evan You 已提交
207
  }
E
Evan You 已提交
208 209 210 211

  return options
}

E
Evan You 已提交
212
async function genComponentRegistrationFile ({ sourceDir }) {
E
Evan You 已提交
213
  function genImport (file) {
E
Evan You 已提交
214
    const name = fileToComponentName(file)
E
Evan You 已提交
215
    const baseDir = path.resolve(sourceDir, '.vuepress/components')
E
Evan You 已提交
216 217 218 219 220
    const absolutePath = path.resolve(baseDir, file)
    const code = `Vue.component(${JSON.stringify(name)}, () => import(${JSON.stringify(absolutePath)}))`
    return code
  }
  const components = (await resolveComponents(sourceDir)) || []
E
Evan You 已提交
221
  return `import Vue from 'vue'\n` + components.map(genImport).join('\n')
E
Evan You 已提交
222 223
}

224
const indexRE = /\b(index|readme)\.md$/i
E
Evan You 已提交
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
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}`
E
Evan You 已提交
248 249 250
}

function isIndexFile (file) {
E
Evan You 已提交
251
  return indexRE.test(file)
E
Evan You 已提交
252 253
}

E
Evan You 已提交
254
async function resolveComponents (sourceDir) {
E
Evan You 已提交
255
  const componentDir = path.resolve(sourceDir, '.vuepress/components')
E
Evan You 已提交
256 257 258
  if (!fs.existsSync(componentDir)) {
    return
  }
E
Evan You 已提交
259
  return sort(await globby(['**/*.vue'], { cwd: componentDir }))
E
Evan You 已提交
260 261
}

E
Evan You 已提交
262 263 264 265
async function genRoutesFile ({ siteData: { pages }, sourceDir, pageFiles }) {
  function genRoute ({ path: pagePath }, index) {
    const file = pageFiles[index]
    const filePath = path.resolve(sourceDir, file)
266
    let code = `
E
Evan You 已提交
267
    {
E
Evan You 已提交
268 269 270 271 272 273 274 275
      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()
        })
      }
E
Evan You 已提交
276
    }`
277 278 279 280 281 282 283 284

    if (/\/$/.test(pagePath)) {
      code += `,{
        path: ${JSON.stringify(pagePath + 'index.html')},
        redirect: ${JSON.stringify(pagePath)}
      }`
    }

E
Evan You 已提交
285 286 287
    return code
  }

E
tweaks  
Evan You 已提交
288
  return (
289
    `import Theme from '@theme'\n` +
E
tweaks  
Evan You 已提交
290 291
    `export const routes = [${pages.map(genRoute).join(',')}\n]`
  )
E
Evan You 已提交
292
}
E
Evan You 已提交
293 294 295 296 297 298 299 300

function sort (arr) {
  return arr.sort((a, b) => {
    if (a < b) return -1
    if (a > b) return 1
    return 0
  })
}
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328

async function parseConfig (file) {
  const content = await fs.readFile(file, 'utf-8')
  const [extension] = /.\w+$/.exec(file)
  let data

  switch (extension) {
  case '.yml':
  case '.yaml':
    data = yamlParser.safeLoad(content)
    break

  case '.toml':
    data = tomlParser.parse(content)
    // reformat to match config since TOML does not allow different data type
    // https://github.com/toml-lang/toml#array
    const format = []
    Object.keys(data.head).forEach(meta => {
      data.head[meta].forEach(values => {
        format.push([meta, values])
      })
    })
    data.head = format
    break
  }

  return data || {}
}