module.exports = async function build (sourceDir, cliOptions = {}) { process.env.NODE_ENV = 'production' const fs = require('fs-extra') const path = require('path') const chalk = require('chalk') const webpack = require('webpack') const readline = require('readline') const escape = require('escape-html') const logger = require('./util/logger') const prepare = require('./prepare') const createClientConfig = require('./webpack/createClientConfig') const createServerConfig = require('./webpack/createServerConfig') const { createBundleRenderer } = require('vue-server-renderer') const { normalizeHeadTag, applyUserWebpackConfig } = require('./util') logger.wait('\nExtracting site metadata...') const options = await prepare(sourceDir) if (cliOptions.outDir) { options.outDir = cliOptions.outDir } const { outDir } = options if (path.resolve() === outDir) { return console.error(logger.error(chalk.red('Unexpected option: outDir cannot be set to the current working directory.\n'), false)) } await fs.remove(outDir) 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) } // compile! const stats = await compile([clientConfig, serverConfig]) const serverBundle = require(path.resolve(outDir, 'manifest/server.json')) const clientManifest = require(path.resolve(outDir, 'manifest/client.json')) // remove manifests after loading them. await fs.remove(path.resolve(outDir, 'manifest')) // find and remove empty style chunk caused by // https://github.com/webpack-contrib/mini-css-extract-plugin/issues/85 // TODO remove when it's fixed await workaroundEmptyStyleChunk() // create server renderer using built manifests const renderer = createBundleRenderer(serverBundle, { clientManifest, runInNewContext: false, inject: false, shouldPrefetch: options.siteConfig.shouldPrefetch || (() => true), template: await fs.readFile(path.resolve(__dirname, 'app/index.ssr.html'), 'utf-8') }) // pre-render head tags from user config const userHeadTags = (options.siteConfig.head || []) .map(renderHeadTag) .join('\n ') // render pages logger.wait('Rendering static HTML...') for (const page of options.siteData.pages) { await renderPage(page) } // 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')) { await renderPage({ path: '/404.html' }) } readline.clearLine(process.stdout, 0) readline.cursorTo(process.stdout, 0) if (options.siteConfig.serviceWorker) { logger.wait('\nGenerating service worker...') const wbb = require('workbox-build') await wbb.generateSW({ swDest: path.resolve(outDir, 'service-worker.js'), globDirectory: outDir, globPatterns: ['**\/*.{js,css,html,png,jpg,jpeg,gif,svg,woff,woff2,eot,ttf,otf}'] }) await fs.writeFile( path.resolve(outDir, 'service-worker.js'), await fs.readFile(path.resolve(__dirname, 'service-worker/skip-waiting.js'), 'utf8'), { flag: 'a' } ) } // DONE. const relativeDir = path.relative(process.cwd(), outDir) logger.success(`\n${chalk.green('Success!')} Generated static files in ${chalk.cyan(relativeDir)}.\n`) // --- helpers --- 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) }) reject(new Error(`Failed to compile with errors.`)) return } if (cliOptions.debug && stats.hasWarnings()) { stats.toJson().warnings.forEach(warning => { console.warn(warning) }) } resolve(stats.toJson({ modules: false })) }) }) } function renderHeadTag (tag) { const { tagName, attributes, innerHTML, closeTag } = normalizeHeadTag(tag) return `<${tagName}${renderAttrs(attributes)}>${innerHTML}${closeTag ? `` : ``}` } function renderAttrs (attrs = {}) { const keys = Object.keys(attrs) if (keys.length) { return ' ' + keys.map(name => `${name}="${escape(attrs[name])}"`).join(' ') } else { return '' } } async function renderPage (page) { const pagePath = page.path readline.clearLine(process.stdout, 0) readline.cursorTo(process.stdout, 0) process.stdout.write(`Rendering page: ${pagePath}`) // #565 Avoid duplicate description meta at SSR. const meta = (page.frontmatter && page.frontmatter.meta || []).filter(item => item.name !== 'description') const pageMeta = renderPageMeta(meta) const context = { url: pagePath, userHeadTags, pageMeta, title: 'VuePress', lang: 'en', description: '' } let html try { html = await renderer.renderToString(context) } catch (e) { console.error(logger.error(chalk.red(`Error rendering ${pagePath}:`), false)) throw e } const filename = decodeURIComponent(pagePath.replace(/\/$/, '/index.html').replace(/^\//, '')) const filePath = path.resolve(outDir, filename) await fs.ensureDir(path.dirname(filePath)) await fs.writeFile(filePath, html) } function renderPageMeta (meta) { if (!meta) return '' return meta.map(m => { let res = ` { res += ` ${key}="${escape(m[key])}"` }) return res + `>` }).join('') } async function workaroundEmptyStyleChunk () { const styleChunk = stats.children[0].assets.find(a => { return /styles\.\w{8}\.js$/.test(a.name) }) if (!styleChunk) return const styleChunkPath = path.resolve(outDir, styleChunk.name) const styleChunkContent = await fs.readFile(styleChunkPath, 'utf-8') await fs.remove(styleChunkPath) // 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) const appChunkContent = await fs.readFile(appChunkPath, 'utf-8') await fs.writeFile(appChunkPath, styleChunkContent + appChunkContent) } }