2017-05-07 22:47:40 +00:00
|
|
|
import del from 'del'
|
|
|
|
import cp from 'recursive-copy'
|
|
|
|
import mkdirp from 'mkdirp-then'
|
2017-05-11 03:01:42 +00:00
|
|
|
import walk from 'walk'
|
2017-10-05 18:33:10 +00:00
|
|
|
import { extname, resolve, join, dirname, sep } from 'path'
|
2017-05-08 06:10:26 +00:00
|
|
|
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
2018-06-04 09:38:46 +00:00
|
|
|
import loadConfig from './config'
|
2018-06-04 13:45:39 +00:00
|
|
|
import {PHASE_EXPORT, SERVER_DIRECTORY, PAGES_MANIFEST, CONFIG_FILE, BUILD_ID_FILE} from '../lib/constants'
|
2017-05-08 06:10:26 +00:00
|
|
|
import { renderToHTML } from './render'
|
2017-06-08 17:41:22 +00:00
|
|
|
import { getAvailableChunks } from './utils'
|
2018-02-03 16:12:01 +00:00
|
|
|
import { setAssetPrefix } from '../lib/asset'
|
2018-02-26 11:03:27 +00:00
|
|
|
import * as envConfig from '../lib/runtime-config'
|
2017-05-07 22:47:40 +00:00
|
|
|
|
2017-09-27 18:48:46 +00:00
|
|
|
export default async function (dir, options, configuration) {
|
2017-05-08 06:10:26 +00:00
|
|
|
dir = resolve(dir)
|
2018-06-04 09:38:46 +00:00
|
|
|
const nextConfig = configuration || loadConfig(PHASE_EXPORT, dir)
|
2018-06-04 13:45:39 +00:00
|
|
|
const distDir = join(dir, nextConfig.distDir)
|
2017-05-07 22:47:40 +00:00
|
|
|
|
2018-06-04 13:45:39 +00:00
|
|
|
log(`> using build directory: ${distDir}`)
|
2017-05-11 16:23:08 +00:00
|
|
|
|
2018-06-04 13:45:39 +00:00
|
|
|
if (!existsSync(distDir)) {
|
|
|
|
throw new Error(`Build directory ${distDir} does not exist. Make sure you run "next build" before running "next start" or "next export".`)
|
2017-05-07 22:47:40 +00:00
|
|
|
}
|
|
|
|
|
2018-06-04 13:45:39 +00:00
|
|
|
const buildId = readFileSync(join(distDir, BUILD_ID_FILE), 'utf8')
|
|
|
|
const pagesManifest = require(join(distDir, SERVER_DIRECTORY, PAGES_MANIFEST))
|
2018-03-30 13:08:09 +00:00
|
|
|
|
|
|
|
const pages = Object.keys(pagesManifest)
|
|
|
|
const defaultPathMap = {}
|
|
|
|
|
|
|
|
for (const page of pages) {
|
2018-04-23 20:27:53 +00:00
|
|
|
// _document and _app are not real pages.
|
|
|
|
if (page === '/_document' || page === '/_app') {
|
2018-03-30 13:08:09 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
defaultPathMap[page] = { page }
|
|
|
|
}
|
2017-05-07 22:47:40 +00:00
|
|
|
|
|
|
|
// Initialize the output directory
|
2017-06-08 01:39:45 +00:00
|
|
|
const outDir = options.outdir
|
2017-05-18 04:28:22 +00:00
|
|
|
await del(join(outDir, '*'))
|
2017-05-07 22:47:40 +00:00
|
|
|
await mkdirp(join(outDir, '_next', buildId))
|
|
|
|
|
2017-05-14 00:11:13 +00:00
|
|
|
// Copy static directory
|
|
|
|
if (existsSync(join(dir, 'static'))) {
|
|
|
|
log(' copying "static" directory')
|
|
|
|
await cp(
|
|
|
|
join(dir, 'static'),
|
2018-01-31 07:38:43 +00:00
|
|
|
join(outDir, 'static'),
|
|
|
|
{ expand: true }
|
2017-05-14 00:11:13 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2018-02-07 10:54:07 +00:00
|
|
|
// Copy .next/static directory
|
2018-06-04 13:45:39 +00:00
|
|
|
if (existsSync(join(distDir, 'static'))) {
|
2018-02-07 10:54:07 +00:00
|
|
|
log(' copying "static build" directory')
|
|
|
|
await cp(
|
2018-06-04 13:45:39 +00:00
|
|
|
join(distDir, 'static'),
|
2018-02-07 10:54:07 +00:00
|
|
|
join(outDir, '_next', 'static')
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2017-05-15 04:33:35 +00:00
|
|
|
// Copy dynamic import chunks
|
2018-06-04 13:45:39 +00:00
|
|
|
if (existsSync(join(distDir, 'chunks'))) {
|
2017-05-15 04:33:35 +00:00
|
|
|
log(' copying dynamic import chunks')
|
|
|
|
|
2017-12-27 18:59:17 +00:00
|
|
|
await mkdirp(join(outDir, '_next', 'webpack'))
|
2017-05-15 04:33:35 +00:00
|
|
|
await cp(
|
2018-06-04 13:45:39 +00:00
|
|
|
join(distDir, 'chunks'),
|
2017-12-27 18:59:17 +00:00
|
|
|
join(outDir, '_next', 'webpack', 'chunks')
|
2017-05-15 04:33:35 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2018-06-04 13:45:39 +00:00
|
|
|
await copyPages(distDir, outDir, buildId)
|
2017-05-08 06:10:26 +00:00
|
|
|
|
2018-06-04 09:38:46 +00:00
|
|
|
// Get the exportPathMap from the config file
|
2018-02-26 11:03:27 +00:00
|
|
|
if (typeof nextConfig.exportPathMap !== 'function') {
|
2018-06-04 09:38:46 +00:00
|
|
|
console.log(`> No "exportPathMap" found in "${CONFIG_FILE}". Generating map from "./pages"`)
|
2018-03-30 13:08:09 +00:00
|
|
|
nextConfig.exportPathMap = async (defaultMap) => {
|
|
|
|
return defaultMap
|
|
|
|
}
|
2017-05-09 01:20:50 +00:00
|
|
|
}
|
|
|
|
|
2017-05-08 06:10:26 +00:00
|
|
|
// Start the rendering process
|
|
|
|
const renderOpts = {
|
|
|
|
dir,
|
|
|
|
buildId,
|
2017-05-09 01:20:50 +00:00
|
|
|
nextExport: true,
|
2018-02-26 11:03:27 +00:00
|
|
|
assetPrefix: nextConfig.assetPrefix.replace(/\/$/, ''),
|
2018-06-04 13:45:39 +00:00
|
|
|
distDir,
|
2017-05-08 06:10:26 +00:00
|
|
|
dev: false,
|
|
|
|
staticMarkup: false,
|
2017-06-08 17:41:22 +00:00
|
|
|
hotReloader: null,
|
Process available chunk names properly in dev mode (#4604)
Fixes #4603.
Tests explain it the best:
```js
describe('development mode (no chunkhash)', () => {
it('should strip the extension from the filename', () => {
const filename = 'foo_bar_0123456789abcdef.js'
expect(getChunkNameFromFilename(filename, true)).toBe('foo_bar_0123456789abcdef')
})
it('should only strip the extension even if there\'s a hyphen in the name', () => {
const filename = 'foo-bar-0123456789abcdef.js'
expect(getChunkNameFromFilename(filename, true)).toBe('foo-bar-0123456789abcdef')
})
})
describe('production mode (with chunkhash)', () => {
it('should strip the hash from the filename', () => {
const filename = 'foo_bar_0123456789abcdef-0123456789abcdef.js'
expect(getChunkNameFromFilename(filename, false)).toBe('foo_bar_0123456789abcdef')
})
it('should only strip the part after the last hyphen in the filename', () => {
const filename = 'foo-bar-0123456789abcdef-0123456789abcdef.js'
expect(getChunkNameFromFilename(filename, false)).toBe('foo-bar-0123456789abcdef')
})
})
```
2018-06-14 09:04:03 +00:00
|
|
|
availableChunks: getAvailableChunks(distDir, false)
|
2018-02-26 11:03:27 +00:00
|
|
|
}
|
|
|
|
|
2018-02-27 16:50:14 +00:00
|
|
|
const {serverRuntimeConfig, publicRuntimeConfig} = nextConfig
|
|
|
|
|
|
|
|
if (publicRuntimeConfig) {
|
|
|
|
renderOpts.runtimeConfig = publicRuntimeConfig
|
2017-05-08 06:10:26 +00:00
|
|
|
}
|
|
|
|
|
2018-02-27 16:50:14 +00:00
|
|
|
envConfig.setConfig({
|
|
|
|
serverRuntimeConfig,
|
|
|
|
publicRuntimeConfig
|
|
|
|
})
|
|
|
|
|
2018-02-03 16:12:01 +00:00
|
|
|
// set the assetPrefix to use for 'next/asset'
|
|
|
|
setAssetPrefix(renderOpts.assetPrefix)
|
|
|
|
|
2017-05-09 01:20:50 +00:00
|
|
|
// We need this for server rendering the Link component.
|
|
|
|
global.__NEXT_DATA__ = {
|
|
|
|
nextExport: true
|
2017-05-08 06:10:26 +00:00
|
|
|
}
|
|
|
|
|
2018-05-11 12:52:39 +00:00
|
|
|
const exportPathMap = await nextConfig.exportPathMap(defaultPathMap)
|
|
|
|
const exportPaths = Object.keys(exportPathMap)
|
|
|
|
|
2017-05-08 06:10:26 +00:00
|
|
|
for (const path of exportPaths) {
|
2018-03-30 13:08:09 +00:00
|
|
|
log(`> exporting path: ${path}`)
|
2017-12-14 21:22:04 +00:00
|
|
|
if (!path.startsWith('/')) {
|
|
|
|
throw new Error(`path "${path}" doesn't start with a backslash`)
|
|
|
|
}
|
2017-05-09 01:53:08 +00:00
|
|
|
|
2017-05-18 05:24:57 +00:00
|
|
|
const { page, query = {} } = exportPathMap[path]
|
2017-05-08 06:10:26 +00:00
|
|
|
const req = { url: path }
|
|
|
|
const res = {}
|
|
|
|
|
2017-10-05 18:33:10 +00:00
|
|
|
let htmlFilename = `${path}${sep}index.html`
|
|
|
|
if (extname(path) !== '') {
|
|
|
|
// If the path has an extension, use that as the filename instead
|
|
|
|
htmlFilename = path
|
|
|
|
} else if (path === '/') {
|
|
|
|
// If the path is the root, just use index.html
|
|
|
|
htmlFilename = 'index.html'
|
|
|
|
}
|
2017-05-08 06:10:26 +00:00
|
|
|
const baseDir = join(outDir, dirname(htmlFilename))
|
|
|
|
const htmlFilepath = join(outDir, htmlFilename)
|
|
|
|
|
|
|
|
await mkdirp(baseDir)
|
|
|
|
|
|
|
|
const html = await renderToHTML(req, res, page, query, renderOpts)
|
|
|
|
writeFileSync(htmlFilepath, html, 'utf8')
|
|
|
|
}
|
2017-05-11 16:23:08 +00:00
|
|
|
|
|
|
|
// Add an empty line to the console for the better readability.
|
|
|
|
log('')
|
|
|
|
|
|
|
|
function log (message) {
|
|
|
|
if (options.silent) return
|
|
|
|
console.log(message)
|
|
|
|
}
|
2017-05-07 22:47:40 +00:00
|
|
|
}
|
2017-05-11 03:01:42 +00:00
|
|
|
|
2018-06-04 13:45:39 +00:00
|
|
|
function copyPages (distDir, outDir, buildId) {
|
2017-05-11 03:01:42 +00:00
|
|
|
// TODO: do some proper error handling
|
|
|
|
return new Promise((resolve, reject) => {
|
2018-06-04 13:45:39 +00:00
|
|
|
const nextBundlesDir = join(distDir, 'bundles', 'pages')
|
2017-05-11 03:01:42 +00:00
|
|
|
const walker = walk.walk(nextBundlesDir, { followLinks: false })
|
|
|
|
|
|
|
|
walker.on('file', (root, stat, next) => {
|
|
|
|
const filename = stat.name
|
|
|
|
const fullFilePath = `${root}${sep}${filename}`
|
|
|
|
const relativeFilePath = fullFilePath.replace(nextBundlesDir, '')
|
|
|
|
|
2017-05-11 15:59:29 +00:00
|
|
|
// We should not expose this page to the client side since
|
|
|
|
// it has no use in the client side.
|
2018-01-31 07:38:43 +00:00
|
|
|
if (relativeFilePath === `${sep}_document.js`) {
|
2017-05-11 15:59:29 +00:00
|
|
|
next()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-05-11 03:01:42 +00:00
|
|
|
let destFilePath = null
|
2017-12-04 23:19:53 +00:00
|
|
|
if (relativeFilePath === `${sep}index.js`) {
|
2017-05-11 03:01:42 +00:00
|
|
|
destFilePath = join(outDir, '_next', buildId, 'page', relativeFilePath)
|
2017-12-04 23:19:53 +00:00
|
|
|
} else if (/index\.js$/.test(filename)) {
|
|
|
|
const newRelativeFilePath = relativeFilePath.replace(`${sep}index.js`, '.js')
|
2017-05-11 03:01:42 +00:00
|
|
|
destFilePath = join(outDir, '_next', buildId, 'page', newRelativeFilePath)
|
2017-12-04 23:19:53 +00:00
|
|
|
} else {
|
|
|
|
destFilePath = join(outDir, '_next', buildId, 'page', relativeFilePath)
|
2017-05-11 03:01:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
cp(fullFilePath, destFilePath)
|
|
|
|
.then(next)
|
|
|
|
.catch(reject)
|
|
|
|
})
|
|
|
|
|
|
|
|
walker.on('end', resolve)
|
|
|
|
})
|
|
|
|
}
|