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
  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
  const enhanceAppPath = path.resolve(sourceDir, '.vuepress/enhanceApp.js')
66 67 68 69 70
  writeEnhanceTemp(
    'enhanceApp.js',
    enhanceAppPath,
    fs.existsSync(enhanceAppPath)
  )
71

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

E
Evan You 已提交
79 80 81 82
  return options
}

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

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

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

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

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

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

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

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

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

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

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

  return options
}

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

241
const indexRE = /\b(index|readme)\.md$/i
E
Evan You 已提交
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
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 已提交
265 266 267
}

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

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

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

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

E
Evan You 已提交
302 303 304
    return code
  }

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

function sort (arr) {
  return arr.sort((a, b) => {
    if (a < b) return -1
    if (a > b) return 1
    return 0
  })
}
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 344 345

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