build.js 6.6 KB
Newer Older
E
cli  
Evan You 已提交
1
module.exports = async function build (sourceDir, cliOptions = {}) {
E
Evan You 已提交
2 3
  process.env.NODE_ENV = 'production'

E
Evan You 已提交
4
  const fs = require('fs-extra')
E
Evan You 已提交
5
  const path = require('path')
E
wip  
Evan You 已提交
6
  const chalk = require('chalk')
E
Evan You 已提交
7
  const webpack = require('webpack')
E
Evan You 已提交
8
  const readline = require('readline')
9
  const escape = require('escape-html')
E
wip  
Evan You 已提交
10

U
ULIVZ 已提交
11
  const logger = require('./util/logger')
U
ULIVZ 已提交
12
  const prepare = require('./prepare/index')
E
Evan You 已提交
13 14
  const createClientConfig = require('./webpack/createClientConfig')
  const createServerConfig = require('./webpack/createServerConfig')
E
wip  
Evan You 已提交
15
  const { createBundleRenderer } = require('vue-server-renderer')
U
ULIVZ 已提交
16
  const { normalizeHeadTag, applyUserWebpackConfig } = require('./util/index')
E
Evan You 已提交
17

U
ULIVZ 已提交
18
  logger.wait('\nExtracting site metadata...')
U
ULIVZ 已提交
19
  const options = await prepare({ sourceDir, cliOptions, isProd: true })
E
cli  
Evan You 已提交
20 21 22
  if (cliOptions.outDir) {
    options.outDir = cliOptions.outDir
  }
E
Evan You 已提交
23

E
Evan You 已提交
24
  const { outDir } = options
25
  if (path.resolve() === outDir) {
U
ULIVZ 已提交
26
    return console.error(logger.error(chalk.red('Unexpected option: outDir cannot be set to the current working directory.\n'), false))
27
  }
E
Evan You 已提交
28
  await fs.remove(outDir)
E
Evan You 已提交
29

E
Evan You 已提交
30 31 32 33 34 35 36 37 38
  let clientConfig = createClientConfig(options, cliOptions).toConfig()
  let serverConfig = createServerConfig(options, cliOptions).toConfig()

  // apply user config...
  const userConfig = options.siteConfig.configureWebpack
  if (userConfig) {
    clientConfig = applyUserWebpackConfig(userConfig, clientConfig, false)
    serverConfig = applyUserWebpackConfig(userConfig, serverConfig, true)
  }
E
Evan You 已提交
39

E
wip  
Evan You 已提交
40
  // compile!
E
Evan You 已提交
41
  const stats = await compile([clientConfig, serverConfig])
E
wip  
Evan You 已提交
42

E
Evan You 已提交
43 44
  const serverBundle = require(path.resolve(outDir, 'manifest/server.json'))
  const clientManifest = require(path.resolve(outDir, 'manifest/client.json'))
E
wip  
Evan You 已提交
45 46

  // remove manifests after loading them.
E
Evan You 已提交
47
  await fs.remove(path.resolve(outDir, 'manifest'))
E
wip  
Evan You 已提交
48

E
Evan You 已提交
49
  // find and remove empty style chunk caused by
E
Evan You 已提交
50
  // https://github.com/webpack-contrib/mini-css-extract-plugin/issues/85
E
Evan You 已提交
51
  // TODO remove when it's fixed
E
Evan You 已提交
52 53
  await workaroundEmptyStyleChunk()

E
Evan You 已提交
54
  // create server renderer using built manifests
E
wip  
Evan You 已提交
55 56 57 58
  const renderer = createBundleRenderer(serverBundle, {
    clientManifest,
    runInNewContext: false,
    inject: false,
59
    shouldPrefetch: options.siteConfig.shouldPrefetch || (() => true),
E
Evan You 已提交
60
    template: await fs.readFile(path.resolve(__dirname, 'app/index.ssr.html'), 'utf-8')
E
wip  
Evan You 已提交
61 62
  })

E
Evan You 已提交
63
  // pre-render head tags from user config
E
wip  
Evan You 已提交
64 65 66 67
  const userHeadTags = (options.siteConfig.head || [])
    .map(renderHeadTag)
    .join('\n  ')

E
Evan You 已提交
68
  // render pages
U
ULIVZ 已提交
69
  logger.wait('Rendering static HTML...')
E
Evan You 已提交
70 71 72
  for (const page of options.siteData.pages) {
    await renderPage(page)
  }
E
wip  
Evan You 已提交
73

E
Evan You 已提交
74 75
  // if the user does not have a custom 404.md, generate the theme's default
  if (!options.siteData.pages.some(p => p.path === '/404.html')) {
E
lint  
Evan You 已提交
76
    await renderPage({ path: '/404.html' })
E
Evan You 已提交
77 78
  }

E
Evan You 已提交
79 80 81
  readline.clearLine(process.stdout, 0)
  readline.cursorTo(process.stdout, 0)

E
Evan You 已提交
82
  if (options.siteConfig.serviceWorker) {
U
ULIVZ 已提交
83
    logger.wait('\nGenerating service worker...')
E
Evan You 已提交
84
    const wbb = require('workbox-build')
85
    await wbb.generateSW({
E
Evan You 已提交
86 87 88 89
      swDest: path.resolve(outDir, 'service-worker.js'),
      globDirectory: outDir,
      globPatterns: ['**\/*.{js,css,html,png,jpg,jpeg,gif,svg,woff,woff2,eot,ttf,otf}']
    })
90 91 92 93 94
    await fs.writeFile(
      path.resolve(outDir, 'service-worker.js'),
      await fs.readFile(path.resolve(__dirname, 'service-worker/skip-waiting.js'), 'utf8'),
      { flag: 'a' }
    )
E
Evan You 已提交
95 96
  }

97
  await options.plugin.options.generated.apply()
98

E
Evan You 已提交
99
  // DONE.
E
cli  
Evan You 已提交
100
  const relativeDir = path.relative(process.cwd(), outDir)
U
ULIVZ 已提交
101
  logger.success(`\n${chalk.green('Success!')} Generated static files in ${chalk.cyan(relativeDir)}.\n`)
E
Evan You 已提交
102 103

  // --- helpers ---
E
Evan You 已提交
104 105 106 107 108 109 110 111 112 113 114

  function compile (config) {
    return new Promise((resolve, reject) => {
      webpack(config, (err, stats) => {
        if (err) {
          return reject(err)
        }
        if (stats.hasErrors()) {
          stats.toJson().errors.forEach(err => {
            console.error(err)
          })
E
tweaks  
Evan You 已提交
115
          reject(new Error(`Failed to compile with errors.`))
E
Evan You 已提交
116 117
          return
        }
118 119 120 121 122
        if (cliOptions.debug && stats.hasWarnings()) {
          stats.toJson().warnings.forEach(warning => {
            console.warn(warning)
          })
        }
E
Evan You 已提交
123
        resolve(stats.toJson({ modules: false }))
E
Evan You 已提交
124 125 126
      })
    })
  }
E
wip  
Evan You 已提交
127

E
Evan You 已提交
128 129 130
  function renderHeadTag (tag) {
    const { tagName, attributes, innerHTML, closeTag } = normalizeHeadTag(tag)
    return `<${tagName}${renderAttrs(attributes)}>${innerHTML}${closeTag ? `</${tagName}>` : ``}`
E
wip  
Evan You 已提交
131 132 133 134 135
  }

  function renderAttrs (attrs = {}) {
    const keys = Object.keys(attrs)
    if (keys.length) {
136
      return ' ' + keys.map(name => `${name}="${escape(attrs[name])}"`).join(' ')
E
wip  
Evan You 已提交
137 138 139 140
    } else {
      return ''
    }
  }
E
Evan You 已提交
141

E
Evan You 已提交
142 143
  async function renderPage (page) {
    const pagePath = page.path
E
Evan You 已提交
144 145 146
    readline.clearLine(process.stdout, 0)
    readline.cursorTo(process.stdout, 0)
    process.stdout.write(`Rendering page: ${pagePath}`)
E
Evan You 已提交
147

U
ULIVZ 已提交
148
    // #565 Avoid duplicate description meta at SSR.
149 150 151
    const meta = (page.frontmatter && page.frontmatter.meta || []).filter(item => item.name !== 'description')
    const pageMeta = renderPageMeta(meta)

E
Evan You 已提交
152 153
    const context = {
      url: pagePath,
E
Evan You 已提交
154
      userHeadTags,
E
Evan You 已提交
155
      pageMeta,
E
Evan You 已提交
156
      title: 'VuePress',
157 158
      lang: 'en',
      description: ''
E
Evan You 已提交
159 160 161 162 163 164
    }

    let html
    try {
      html = await renderer.renderToString(context)
    } catch (e) {
U
ULIVZ 已提交
165
      console.error(logger.error(chalk.red(`Error rendering ${pagePath}:`), false))
E
Evan You 已提交
166
      throw e
E
Evan You 已提交
167
    }
168
    const filename = decodeURIComponent(pagePath.replace(/\/$/, '/index.html').replace(/^\//, ''))
E
Evan You 已提交
169
    const filePath = path.resolve(outDir, filename)
E
Evan You 已提交
170 171
    await fs.ensureDir(path.dirname(filePath))
    await fs.writeFile(filePath, html)
E
Evan You 已提交
172
  }
E
Evan You 已提交
173

E
Evan You 已提交
174 175 176 177 178
  function renderPageMeta (meta) {
    if (!meta) return ''
    return meta.map(m => {
      let res = `<meta`
      Object.keys(m).forEach(key => {
179
        res += ` ${key}="${escape(m[key])}"`
E
Evan You 已提交
180 181 182 183 184
      })
      return res + `>`
    }).join('')
  }

E
Evan You 已提交
185 186 187 188
  async function workaroundEmptyStyleChunk () {
    const styleChunk = stats.children[0].assets.find(a => {
      return /styles\.\w{8}\.js$/.test(a.name)
    })
189
    if (!styleChunk) return
E
Evan You 已提交
190
    const styleChunkPath = path.resolve(outDir, styleChunk.name)
E
Evan You 已提交
191 192
    const styleChunkContent = await fs.readFile(styleChunkPath, 'utf-8')
    await fs.remove(styleChunkPath)
E
Evan You 已提交
193 194 195 196 197 198
    // prepend it to app.js.
    // this is necessary for the webpack runtime to work properly.
    const appChunk = stats.children[0].assets.find(a => {
      return /app\.\w{8}\.js$/.test(a.name)
    })
    const appChunkPath = path.resolve(outDir, appChunk.name)
E
Evan You 已提交
199 200
    const appChunkContent = await fs.readFile(appChunkPath, 'utf-8')
    await fs.writeFile(appChunkPath, styleChunkContent + appChunkContent)
E
Evan You 已提交
201
  }
E
Evan You 已提交
202
}