prepare.js 9.6 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'))
  )

114 115 116 117 118 119 120
  // resolve algolia
  const isAlgoliaSearch = (
    siteConfig.algolia ||
    Object.keys(siteConfig.locales && siteConfig.themeConfig && siteConfig.themeConfig.locales || {})
      .some(base => siteConfig.themeConfig.locales[base].algolia)
  )

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

E
Evan You 已提交
137
  if (useDefaultTheme) {
E
Evan You 已提交
138 139 140 141
    // 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 已提交
142 143
    let themeDir
    let themePath
E
Evan You 已提交
144
    // resolve custom theme
E
Evan You 已提交
145 146 147 148 149 150 151 152 153
    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 已提交
154
    } else {
E
Evan You 已提交
155 156 157 158 159
      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 已提交
160
    }
E
Evan You 已提交
161
    options.themePath = themePath
E
Evan You 已提交
162

163
    const notFoundPath = path.resolve(themeDir, 'NotFound.vue')
E
Evan You 已提交
164 165 166
    if (fs.existsSync(notFoundPath)) {
      options.notFoundPath = notFoundPath
    } else {
E
Evan You 已提交
167
      options.notFoundPath = path.resolve(__dirname, 'default-theme/NotFound.vue')
E
Evan You 已提交
168
    }
169 170 171 172 173

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

E
Evan You 已提交
176
  // resolve pages
E
Evan You 已提交
177
  const pagesData = await Promise.all(options.pageFiles.map(async (file) => {
E
tweaks  
Evan You 已提交
178
    const data = {
E
Evan You 已提交
179
      path: fileToPath(file)
E
Evan You 已提交
180
    }
E
Evan You 已提交
181 182

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

E
Evan You 已提交
207
  // resolve site data
E
Evan You 已提交
208
  options.siteData = {
E
Evan You 已提交
209 210
    title: siteConfig.title || '',
    description: siteConfig.description || '',
E
Evan You 已提交
211
    base: siteConfig.base || '/',
E
Evan You 已提交
212
    pages: pagesData,
213
    themeConfig: siteConfig.themeConfig || {},
E
Evan You 已提交
214
    locales: siteConfig.locales
E
Evan You 已提交
215
  }
E
Evan You 已提交
216 217 218 219

  return options
}

E
Evan You 已提交
220
async function genComponentRegistrationFile ({ sourceDir }) {
E
Evan You 已提交
221
  function genImport (file) {
E
Evan You 已提交
222
    const name = fileToComponentName(file)
E
Evan You 已提交
223
    const baseDir = path.resolve(sourceDir, '.vuepress/components')
E
Evan You 已提交
224 225 226 227 228
    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 已提交
229
  return `import Vue from 'vue'\n` + components.map(genImport).join('\n')
E
Evan You 已提交
230 231
}

232
const indexRE = /\b(index|readme)\.md$/i
E
Evan You 已提交
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
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 已提交
256 257 258
}

function isIndexFile (file) {
E
Evan You 已提交
259
  return indexRE.test(file)
E
Evan You 已提交
260 261
}

E
Evan You 已提交
262
async function resolveComponents (sourceDir) {
E
Evan You 已提交
263
  const componentDir = path.resolve(sourceDir, '.vuepress/components')
E
Evan You 已提交
264 265 266
  if (!fs.existsSync(componentDir)) {
    return
  }
E
Evan You 已提交
267
  return sort(await globby(['**/*.vue'], { cwd: componentDir }))
E
Evan You 已提交
268 269
}

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

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

E
Evan You 已提交
293 294 295
    return code
  }

E
tweaks  
Evan You 已提交
296
  return (
297
    `import Theme from '@theme'\n` +
E
tweaks  
Evan You 已提交
298 299
    `export const routes = [${pages.map(genRoute).join(',')}\n]`
  )
E
Evan You 已提交
300
}
E
Evan You 已提交
301 302 303 304 305 306 307 308

function sort (arr) {
  return arr.sort((a, b) => {
    if (a < b) return -1
    if (a > b) return 1
    return 0
  })
}
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336

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