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 123
  // resolve algolia
  const isAlgoliaSearch = (
124
    siteConfig.themeConfig.algolia ||
125 126 127 128
    Object.keys(siteConfig.locales && siteConfig.themeConfig && siteConfig.themeConfig.locales || {})
      .some(base => siteConfig.themeConfig.locales[base].algolia)
  )

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

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

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

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

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

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

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

  return options
}

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

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

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

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

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

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

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

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

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

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