prepare.js 9.7 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 50 51
  const overridePath = path.resolve(sourceDir, '.vuepress/override.styl')
  const hasUserOverride = options.useDefaultTheme && fs.existsSync(overridePath)
  await writeTemp(`override.styl`, hasUserOverride ? `@import(${JSON.stringify(overridePath)})` : ``)
E
Evan You 已提交
52

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

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

70 71 72 73 74 75
  // 7. handle the theme enhanceApp.js
  writeEnhanceTemp(
    'themeEnhanceApp.js',
    options.themeEnhanceAppPath,
    fs.existsSync(options.themeEnhanceAppPath)
  )
方剑成 已提交
76

E
Evan You 已提交
77 78 79 80
  return options
}

async function resolveOptions (sourceDir) {
E
Evan You 已提交
81 82
  const vuepressDir = path.resolve(sourceDir, '.vuepress')
  const configPath = path.resolve(vuepressDir, 'config.js')
A
Aliex Leung 已提交
83
  const configYmlPath = path.resolve(vuepressDir, 'config.yml')
84
  const configTomlPath = path.resolve(vuepressDir, 'config.toml')
85 86

  delete require.cache[configPath]
A
Aliex Leung 已提交
87 88
  let siteConfig = {}
  if (fs.existsSync(configYmlPath)) {
89 90 91
    siteConfig = await parseConfig(configYmlPath)
  } else if (fs.existsSync(configTomlPath)) {
    siteConfig = await parseConfig(configTomlPath)
A
Aliex Leung 已提交
92 93 94
  } else if (fs.existsSync(configPath)) {
    siteConfig = require(configPath)
  }
E
Evan You 已提交
95

E
Evan You 已提交
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
  // 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 已提交
114 115 116 117 118 119
  // resolve theme
  const useDefaultTheme = (
    !siteConfig.theme &&
    !fs.existsSync(path.resolve(vuepressDir, 'theme'))
  )

120
  // resolve algolia
E
Evan You 已提交
121
  const themeConfig = siteConfig.themeConfig || {}
122
  const isAlgoliaSearch = (
E
Evan You 已提交
123 124 125
    themeConfig.algolia ||
    Object.keys(siteConfig.locales && themeConfig.locales || {})
      .some(base => themeConfig.locales[base].algolia)
126 127
  )

E
Evan You 已提交
128
  const options = {
E
Evan You 已提交
129
    siteConfig,
E
Evan You 已提交
130
    sourceDir,
E
Evan You 已提交
131 132 133
    outDir: siteConfig.dest
      ? path.resolve(siteConfig.dest)
      : path.resolve(sourceDir, '.vuepress/dist'),
E
Evan You 已提交
134
    publicPath: base,
135
    pageFiles: sort(await globby(['**/*.md', '!.vuepress', '!node_modules'], { cwd: sourceDir })),
E
Evan You 已提交
136 137
    pagesData: null,
    themePath: null,
E
Evan You 已提交
138
    notFoundPath: null,
139
    useDefaultTheme,
140
    isAlgoliaSearch,
141
    markdown: createMarkdown(siteConfig)
E
Evan You 已提交
142 143
  }

E
Evan You 已提交
144
  if (useDefaultTheme) {
E
Evan You 已提交
145 146 147 148
    // 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 已提交
149 150
    let themeDir
    let themePath
E
Evan You 已提交
151
    // resolve custom theme
E
Evan You 已提交
152 153 154 155 156 157 158 159 160
    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 已提交
161
    } else {
E
Evan You 已提交
162 163 164 165 166
      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 已提交
167
    }
E
Evan You 已提交
168
    options.themePath = themePath
E
Evan You 已提交
169

170
    const notFoundPath = path.resolve(themeDir, 'NotFound.vue')
E
Evan You 已提交
171 172 173
    if (fs.existsSync(notFoundPath)) {
      options.notFoundPath = notFoundPath
    } else {
E
Evan You 已提交
174
      options.notFoundPath = path.resolve(__dirname, 'default-theme/NotFound.vue')
E
Evan You 已提交
175
    }
176

177
    const themeEnhanceAppPath = path.resolve(themeDir, 'enhanceApp.js')
178 179
    if (fs.existsSync(themeEnhanceAppPath)) {
      options.themeEnhanceAppPath = themeEnhanceAppPath
180
    }
E
tweaks  
Evan You 已提交
181 182
  }

E
Evan You 已提交
183
  // resolve pages
E
Evan You 已提交
184
  const pagesData = await Promise.all(options.pageFiles.map(async (file) => {
E
tweaks  
Evan You 已提交
185
    const data = {
E
Evan You 已提交
186
      path: fileToPath(file)
E
Evan You 已提交
187
    }
E
Evan You 已提交
188 189

    // extract yaml frontmatter
E
Evan You 已提交
190
    const content = await fs.readFile(path.resolve(sourceDir, file), 'utf-8')
191
    const frontmatter = parseFrontmatter(content)
E
Evan You 已提交
192
    // infer title
E
Evan You 已提交
193 194 195
    const title = inferTitle(frontmatter)
    if (title) {
      data.title = title
E
Evan You 已提交
196
    }
197
    const headers = extractHeaders(
198
      frontmatter.content,
199 200 201
      ['h2', 'h3'],
      options.markdown
    )
E
Evan You 已提交
202 203 204
    if (headers.length) {
      data.headers = headers
    }
205 206
    if (Object.keys(frontmatter.data).length) {
      data.frontmatter = frontmatter.data
E
Evan You 已提交
207
    }
208 209 210
    if (frontmatter.excerpt) {
      data.excerpt = frontmatter.excerpt
    }
E
tweaks  
Evan You 已提交
211
    return data
E
Evan You 已提交
212
  }))
E
Evan You 已提交
213

E
Evan You 已提交
214
  // resolve site data
E
Evan You 已提交
215
  options.siteData = {
E
Evan You 已提交
216 217
    title: siteConfig.title || '',
    description: siteConfig.description || '',
E
Evan You 已提交
218
    base: siteConfig.base || '/',
E
Evan You 已提交
219
    pages: pagesData,
E
Evan You 已提交
220
    themeConfig,
E
Evan You 已提交
221
    locales: siteConfig.locales
E
Evan You 已提交
222
  }
E
Evan You 已提交
223 224 225 226

  return options
}

E
Evan You 已提交
227
async function genComponentRegistrationFile ({ sourceDir }) {
E
Evan You 已提交
228
  function genImport (file) {
E
Evan You 已提交
229
    const name = fileToComponentName(file)
E
Evan You 已提交
230
    const baseDir = path.resolve(sourceDir, '.vuepress/components')
E
Evan You 已提交
231 232 233 234 235
    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 已提交
236
  return `import Vue from 'vue'\n` + components.map(genImport).join('\n')
E
Evan You 已提交
237 238
}

239
const indexRE = /(^|.*\/)(index|readme)\.md$/i
E
Evan You 已提交
240 241 242 243 244 245
const extRE = /\.(vue|md)$/

function fileToPath (file) {
  if (isIndexFile(file)) {
    // README.md -> /
    // foo/README.md -> /foo/
246
    return file.replace(indexRE, '/$1')
E
Evan You 已提交
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
  } 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 已提交
263 264 265
}

function isIndexFile (file) {
E
Evan You 已提交
266
  return indexRE.test(file)
E
Evan You 已提交
267 268
}

E
Evan You 已提交
269
async function resolveComponents (sourceDir) {
E
Evan You 已提交
270
  const componentDir = path.resolve(sourceDir, '.vuepress/components')
E
Evan You 已提交
271 272 273
  if (!fs.existsSync(componentDir)) {
    return
  }
E
Evan You 已提交
274
  return sort(await globby(['**/*.vue'], { cwd: componentDir }))
E
Evan You 已提交
275 276
}

E
Evan You 已提交
277 278 279 280
async function genRoutesFile ({ siteData: { pages }, sourceDir, pageFiles }) {
  function genRoute ({ path: pagePath }, index) {
    const file = pageFiles[index]
    const filePath = path.resolve(sourceDir, file)
281
    let code = `
E
Evan You 已提交
282
    {
E
Evan You 已提交
283 284 285 286 287 288 289 290
      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 已提交
291
    }`
292 293 294 295 296 297 298 299

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

E
Evan You 已提交
300 301 302
    return code
  }

E
tweaks  
Evan You 已提交
303
  return (
304
    `import Theme from '@theme'\n` +
E
tweaks  
Evan You 已提交
305 306
    `export const routes = [${pages.map(genRoute).join(',')}\n]`
  )
E
Evan You 已提交
307
}
E
Evan You 已提交
308 309 310 311 312 313 314 315

function sort (arr) {
  return arr.sort((a, b) => {
    if (a < b) return -1
    if (a > b) return 1
    return 0
  })
}
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343

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 || {}
}