1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

Compile pages to .next/static directory (#4828)

* Compile pages to .next/static/<buildid>/pages/<page>

* Fix test

* Export class instead of using exports

* Use constant for static directory

* Add comment about what the middleware does
This commit is contained in:
Tim Neutkens 2018-07-25 13:45:42 +02:00 committed by GitHub
parent c090a57e77
commit 475b426ed1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 134 additions and 178 deletions

View file

@ -128,7 +128,7 @@ export default async function getBaseWebpackConfig (dir: string, {dev = false, i
.filter((p) => !!p) .filter((p) => !!p)
const outputPath = path.join(dir, config.distDir, isServer ? SERVER_DIRECTORY : '') const outputPath = path.join(dir, config.distDir, isServer ? SERVER_DIRECTORY : '')
const pagesEntries = await getPages(dir, {nextPagesDir: DEFAULT_PAGES_DIR, dev, isServer, pageExtensions: config.pageExtensions.join('|')}) const pagesEntries = await getPages(dir, {nextPagesDir: DEFAULT_PAGES_DIR, dev, buildId, isServer, pageExtensions: config.pageExtensions.join('|')})
const totalPages = Object.keys(pagesEntries).length const totalPages = Object.keys(pagesEntries).length
const clientEntries = !isServer ? { const clientEntries = !isServer ? {
// Backwards compatibility // Backwards compatibility

View file

@ -3,7 +3,7 @@ import { RawSource } from 'webpack-sources'
import {PAGES_MANIFEST, ROUTE_NAME_REGEX} from '../../../lib/constants' import {PAGES_MANIFEST, ROUTE_NAME_REGEX} from '../../../lib/constants'
// This plugin creates a pages-manifest.json from page entrypoints. // This plugin creates a pages-manifest.json from page entrypoints.
// This is used for mapping paths like `/` to `.next/dist/bundles/pages/index.js` when doing SSR // This is used for mapping paths like `/` to `.next/server/static/<buildid>/pages/index.js` when doing SSR
// It's also used by next export to provide defaultPathMap // It's also used by next export to provide defaultPathMap
export default class PagesManifestPlugin { export default class PagesManifestPlugin {
apply (compiler: any) { apply (compiler: any) {

View file

@ -33,7 +33,7 @@ function buildManifest (compiler, compilation) {
return manifest return manifest
} }
class ReactLoadablePlugin { export class ReactLoadablePlugin {
constructor (opts = {}) { constructor (opts = {}) {
this.filename = opts.filename this.filename = opts.filename
} }
@ -54,5 +54,3 @@ class ReactLoadablePlugin {
}) })
} }
} }
exports.ReactLoadablePlugin = ReactLoadablePlugin

View file

@ -1,13 +1,14 @@
import path from 'path' import path from 'path'
import promisify from '../../lib/promisify' import promisify from '../../lib/promisify'
import globModule from 'glob' import globModule from 'glob'
import {CLIENT_STATIC_FILES_PATH} from '../../lib/constants'
const glob = promisify(globModule) const glob = promisify(globModule)
export async function getPages (dir, {nextPagesDir, dev, isServer, pageExtensions}) { export async function getPages (dir, {nextPagesDir, dev, buildId, isServer, pageExtensions}) {
const pageFiles = await getPagePaths(dir, {dev, isServer, pageExtensions}) const pageFiles = await getPagePaths(dir, {dev, isServer, pageExtensions})
return getPageEntries(pageFiles, {nextPagesDir, isServer, pageExtensions}) return getPageEntries(pageFiles, {nextPagesDir, buildId, isServer, pageExtensions})
} }
export async function getPagePaths (dir, {dev, isServer, pageExtensions}) { export async function getPagePaths (dir, {dev, isServer, pageExtensions}) {
@ -25,7 +26,7 @@ export async function getPagePaths (dir, {dev, isServer, pageExtensions}) {
} }
// Convert page path into single entry // Convert page path into single entry
export function createEntry (filePath, {name, pageExtensions} = {}) { export function createEntry (filePath, {buildId = '', name, pageExtensions} = {}) {
const parsedPath = path.parse(filePath) const parsedPath = path.parse(filePath)
let entryName = name || filePath let entryName = name || filePath
@ -41,35 +42,35 @@ export function createEntry (filePath, {name, pageExtensions} = {}) {
} }
return { return {
name: path.join('bundles', entryName), name: path.join(CLIENT_STATIC_FILES_PATH, buildId, entryName),
files: [parsedPath.root ? filePath : `./${filePath}`] // The entry always has to be an array. files: [parsedPath.root ? filePath : `./${filePath}`] // The entry always has to be an array.
} }
} }
// Convert page paths into entries // Convert page paths into entries
export function getPageEntries (pagePaths, {nextPagesDir, isServer = false, pageExtensions} = {}) { export function getPageEntries (pagePaths, {nextPagesDir, buildId, isServer = false, pageExtensions} = {}) {
const entries = {} const entries = {}
for (const filePath of pagePaths) { for (const filePath of pagePaths) {
const entry = createEntry(filePath, {pageExtensions}) const entry = createEntry(filePath, {pageExtensions, buildId})
entries[entry.name] = entry.files entries[entry.name] = entry.files
} }
const appPagePath = path.join(nextPagesDir, '_app.js') const appPagePath = path.join(nextPagesDir, '_app.js')
const appPageEntry = createEntry(appPagePath, {name: 'pages/_app.js'}) // default app.js const appPageEntry = createEntry(appPagePath, {buildId, name: 'pages/_app.js'}) // default app.js
if (!entries[appPageEntry.name]) { if (!entries[appPageEntry.name]) {
entries[appPageEntry.name] = appPageEntry.files entries[appPageEntry.name] = appPageEntry.files
} }
const errorPagePath = path.join(nextPagesDir, '_error.js') const errorPagePath = path.join(nextPagesDir, '_error.js')
const errorPageEntry = createEntry(errorPagePath, {name: 'pages/_error.js'}) // default error.js const errorPageEntry = createEntry(errorPagePath, {buildId, name: 'pages/_error.js'}) // default error.js
if (!entries[errorPageEntry.name]) { if (!entries[errorPageEntry.name]) {
entries[errorPageEntry.name] = errorPageEntry.files entries[errorPageEntry.name] = errorPageEntry.files
} }
if (isServer) { if (isServer) {
const documentPagePath = path.join(nextPagesDir, '_document.js') const documentPagePath = path.join(nextPagesDir, '_document.js')
const documentPageEntry = createEntry(documentPagePath, {name: 'pages/_document.js'}) // default _document.js const documentPageEntry = createEntry(documentPagePath, {buildId, name: 'pages/_document.js'}) // default _document.js
if (!entries[documentPageEntry.name]) { if (!entries[documentPageEntry.name]) {
entries[documentPageEntry.name] = documentPageEntry.files entries[documentPageEntry.name] = documentPageEntry.files
} }

View file

@ -14,9 +14,12 @@ export const BLOCKED_PAGES = [
'/_app', '/_app',
'/_error' '/_error'
] ]
export const IS_BUNDLED_PAGE_REGEX = /^bundles[/\\]pages.*\.js$/ // matches static/<buildid>/pages/<page>.js
export const ROUTE_NAME_REGEX = /^bundles[/\\]pages[/\\](.*)\.js$/ export const IS_BUNDLED_PAGE_REGEX = /^static[/\\][^/\\]+[/\\]pages.*\.js$/
// matches static/<buildid>/pages/:page*.js
export const ROUTE_NAME_REGEX = /^static[/\\][^/\\]+[/\\]pages[/\\](.*)\.js$/
export const NEXT_PROJECT_ROOT = join(__dirname, '..', '..') export const NEXT_PROJECT_ROOT = join(__dirname, '..', '..')
export const NEXT_PROJECT_ROOT_DIST = join(NEXT_PROJECT_ROOT, 'dist') export const NEXT_PROJECT_ROOT_DIST = join(NEXT_PROJECT_ROOT, 'dist')
export const NEXT_PROJECT_ROOT_NODE_MODULES = join(NEXT_PROJECT_ROOT, 'node_modules') export const NEXT_PROJECT_ROOT_NODE_MODULES = join(NEXT_PROJECT_ROOT, 'node_modules')
export const DEFAULT_PAGES_DIR = join(NEXT_PROJECT_ROOT_DIST, 'pages') export const DEFAULT_PAGES_DIR = join(NEXT_PROJECT_ROOT_DIST, 'pages')
export const CLIENT_STATIC_FILES_PATH = 'static'

View file

@ -69,7 +69,7 @@ export default class PageLoader {
const scriptRoute = route === '/' ? '/index.js' : `${route}.js` const scriptRoute = route === '/' ? '/index.js' : `${route}.js`
const script = document.createElement('script') const script = document.createElement('script')
const url = `${this.assetPrefix}/_next/${encodeURIComponent(this.buildId)}/page${scriptRoute}` const url = `${this.assetPrefix}/_next/static/${encodeURIComponent(this.buildId)}/pages${scriptRoute}`
script.src = url script.src = url
script.onerror = () => { script.onerror = () => {
const error = new Error(`Error when loading route: ${route}`) const error = new Error(`Error when loading route: ${route}`)

View file

@ -99,7 +99,6 @@
"unfetch": "3.0.0", "unfetch": "3.0.0",
"url": "0.11.0", "url": "0.11.0",
"uuid": "3.1.0", "uuid": "3.1.0",
"walk": "2.3.9",
"webpack": "4.16.1", "webpack": "4.16.1",
"webpack-dev-middleware": "3.1.3", "webpack-dev-middleware": "3.1.3",
"webpack-hot-middleware": "2.22.2", "webpack-hot-middleware": "2.22.2",

View file

@ -80,9 +80,9 @@ export class Head extends Component {
return <head {...this.props}> return <head {...this.props}>
{(head || []).map((h, i) => React.cloneElement(h, { key: h.key || i }))} {(head || []).map((h, i) => React.cloneElement(h, { key: h.key || i }))}
{page !== '/_error' && <link rel='preload' href={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} as='script' nonce={this.props.nonce} />} {page !== '/_error' && <link rel='preload' href={`${assetPrefix}/_next/static/${buildId}/pages${pagePathname}`} as='script' nonce={this.props.nonce} />}
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page/_app.js`} as='script' nonce={this.props.nonce} /> <link rel='preload' href={`${assetPrefix}/_next/static/${buildId}/pages/_app.js`} as='script' nonce={this.props.nonce} />
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page/_error.js`} as='script' nonce={this.props.nonce} /> <link rel='preload' href={`${assetPrefix}/_next/static/${buildId}/pages/_error.js`} as='script' nonce={this.props.nonce} />
{this.getPreloadDynamicChunks()} {this.getPreloadDynamicChunks()}
{this.getPreloadMainLinks()} {this.getPreloadMainLinks()}
{styles || null} {styles || null}
@ -168,9 +168,9 @@ export class NextScript extends Component {
})`: ''} })`: ''}
` `
}} />} }} />}
{page !== '/_error' && <script async id={`__NEXT_PAGE__${pathname}`} src={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} nonce={this.props.nonce} />} {page !== '/_error' && <script async id={`__NEXT_PAGE__${pathname}`} src={`${assetPrefix}/_next/static/${buildId}/pages${pagePathname}`} nonce={this.props.nonce} />}
<script async id={`__NEXT_PAGE__/_app`} src={`${assetPrefix}/_next/${buildId}/page/_app.js`} nonce={this.props.nonce} /> <script async id={`__NEXT_PAGE__/_app`} src={`${assetPrefix}/_next/static/${buildId}/pages/_app.js`} nonce={this.props.nonce} />
<script async id={`__NEXT_PAGE__/_error`} src={`${assetPrefix}/_next/${buildId}/page/_error.js`} nonce={this.props.nonce} /> <script async id={`__NEXT_PAGE__/_error`} src={`${assetPrefix}/_next/static/${buildId}/pages/_error.js`} nonce={this.props.nonce} />
{staticMarkup ? null : this.getDynamicChunks()} {staticMarkup ? null : this.getDynamicChunks()}
{staticMarkup ? null : this.getScripts()} {staticMarkup ? null : this.getScripts()}
</Fragment> </Fragment>

View file

@ -1,11 +1,10 @@
import del from 'del' import del from 'del'
import cp from 'recursive-copy' import cp from 'recursive-copy'
import mkdirp from 'mkdirp-then' import mkdirp from 'mkdirp-then'
import walk from 'walk'
import { extname, resolve, join, dirname, sep } from 'path' import { extname, resolve, join, dirname, sep } from 'path'
import { existsSync, readFileSync, writeFileSync } from 'fs' import { existsSync, readFileSync, writeFileSync } from 'fs'
import loadConfig from './config' import loadConfig from './config'
import {PHASE_EXPORT, SERVER_DIRECTORY, PAGES_MANIFEST, CONFIG_FILE, BUILD_ID_FILE} from '../lib/constants' import {PHASE_EXPORT, SERVER_DIRECTORY, PAGES_MANIFEST, CONFIG_FILE, BUILD_ID_FILE, CLIENT_STATIC_FILES_PATH} from '../lib/constants'
import { renderToHTML } from './render' import { renderToHTML } from './render'
import { setAssetPrefix } from '../lib/asset' import { setAssetPrefix } from '../lib/asset'
import * as envConfig from '../lib/runtime-config' import * as envConfig from '../lib/runtime-config'
@ -51,11 +50,11 @@ export default async function (dir, options, configuration) {
} }
// Copy .next/static directory // Copy .next/static directory
if (existsSync(join(distDir, 'static'))) { if (existsSync(join(distDir, CLIENT_STATIC_FILES_PATH))) {
log(' copying "static build" directory') log(' copying "static build" directory')
await cp( await cp(
join(distDir, 'static'), join(distDir, CLIENT_STATIC_FILES_PATH),
join(outDir, '_next', 'static') join(outDir, '_next', CLIENT_STATIC_FILES_PATH)
) )
} }
@ -70,8 +69,6 @@ export default async function (dir, options, configuration) {
) )
} }
await copyPages(distDir, outDir, buildId)
// Get the exportPathMap from the config file // Get the exportPathMap from the config file
if (typeof nextConfig.exportPathMap !== 'function') { if (typeof nextConfig.exportPathMap !== 'function') {
console.log(`> No "exportPathMap" found in "${CONFIG_FILE}". Generating map from "./pages"`) console.log(`> No "exportPathMap" found in "${CONFIG_FILE}". Generating map from "./pages"`)
@ -149,40 +146,3 @@ export default async function (dir, options, configuration) {
console.log(message) console.log(message)
} }
} }
function copyPages (distDir, outDir, buildId) {
// TODO: do some proper error handling
return new Promise((resolve, reject) => {
const nextBundlesDir = join(distDir, 'bundles', 'pages')
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, '')
// We should not expose this page to the client side since
// it has no use in the client side.
if (relativeFilePath === `${sep}_document.js`) {
next()
return
}
let destFilePath = null
if (relativeFilePath === `${sep}index.js`) {
destFilePath = join(outDir, '_next', buildId, 'page', relativeFilePath)
} else if (/index\.js$/.test(filename)) {
const newRelativeFilePath = relativeFilePath.replace(`${sep}index.js`, '.js')
destFilePath = join(outDir, '_next', buildId, 'page', newRelativeFilePath)
} else {
destFilePath = join(outDir, '_next', buildId, 'page', relativeFilePath)
}
cp(fullFilePath, destFilePath)
.then(next)
.catch(reject)
})
walker.on('end', resolve)
})
}

View file

@ -8,7 +8,12 @@ import getBaseWebpackConfig from '../build/webpack'
import { import {
addCorsSupport addCorsSupport
} from './utils' } from './utils'
import {IS_BUNDLED_PAGE_REGEX, ROUTE_NAME_REGEX} from '../lib/constants' import {IS_BUNDLED_PAGE_REGEX, ROUTE_NAME_REGEX, BLOCKED_PAGES, CLIENT_STATIC_FILES_PATH} from '../lib/constants'
import pathMatch from './lib/path-match'
import {renderScriptError} from './render'
const route = pathMatch()
const matchNextPageBundleRequest = route('/_next/static/:buildId/pages/:path*.js(.map)?')
// Recursively look up the issuer till it ends up at the root // Recursively look up the issuer till it ends up at the root
function findEntryModule (issuer) { function findEntryModule (issuer) {
@ -46,7 +51,7 @@ function erroredPages (compilation, options = {enhanceName: (name) => name}) {
} }
export default class HotReloader { export default class HotReloader {
constructor (dir, { quiet, config, buildId } = {}) { constructor (dir, { config, buildId } = {}) {
this.buildId = buildId this.buildId = buildId
this.dir = dir this.dir = dir
this.middlewares = [] this.middlewares = []
@ -63,7 +68,7 @@ export default class HotReloader {
this.config = config this.config = config
} }
async run (req, res) { async run (req, res, parsedUrl) {
// Usually CORS support is not needed for the hot-reloader (this is dev only feature) // Usually CORS support is not needed for the hot-reloader (this is dev only feature)
// With when the app runs for multi-zones support behind a proxy, // With when the app runs for multi-zones support behind a proxy,
// the current page is trying to access this URL via assetPrefix. // the current page is trying to access this URL via assetPrefix.
@ -73,6 +78,42 @@ export default class HotReloader {
return return
} }
// When a request comes in that is a page bundle, e.g. /_next/static/<buildid>/pages/index.js
// we have to compile the page using on-demand-entries, this middleware will handle doing that
// by adding the page to on-demand-entries, waiting till it's done
// and then the bundle will be served like usual by the actual route in server/index.js
const handlePageBundleRequest = async (req, res, parsedUrl) => {
const {pathname} = parsedUrl
const params = matchNextPageBundleRequest(pathname)
if (!params) {
return {}
}
if (params.buildId !== this.buildId) {
return
}
const page = `/${params.path.join('/')}`
if (BLOCKED_PAGES.indexOf(page) === -1) {
try {
await this.ensurePage(page)
} catch (error) {
await renderScriptError(req, res, page, error)
return {finished: true}
}
const errors = await this.getCompilationErrors(page)
if (errors.length > 0) {
await renderScriptError(req, res, page, errors[0])
return {finished: true}
}
}
return {}
}
const {finished} = await handlePageBundleRequest(req, res, parsedUrl)
for (const fn of this.middlewares) { for (const fn of this.middlewares) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
fn(req, res, (err) => { fn(req, res, (err) => {
@ -81,6 +122,8 @@ export default class HotReloader {
}) })
}) })
} }
return {finished}
} }
async clean () { async clean () {
@ -121,8 +164,8 @@ export default class HotReloader {
await this.clean() await this.clean()
const configs = await Promise.all([ const configs = await Promise.all([
getBaseWebpackConfig(this.dir, { dev: true, isServer: false, config: this.config }), getBaseWebpackConfig(this.dir, { dev: true, isServer: false, config: this.config, buildId: this.buildId }),
getBaseWebpackConfig(this.dir, { dev: true, isServer: true, config: this.config }) getBaseWebpackConfig(this.dir, { dev: true, isServer: true, config: this.config, buildId: this.buildId })
]) ])
const compiler = webpack(configs) const compiler = webpack(configs)
@ -158,7 +201,7 @@ export default class HotReloader {
// We only watch `_document` for changes on the server compilation // We only watch `_document` for changes on the server compilation
// the rest of the files will be triggered by the client compilation // the rest of the files will be triggered by the client compilation
const documentChunk = compilation.chunks.find(c => c.name === normalize('bundles/pages/_document.js')) const documentChunk = compilation.chunks.find(c => c.name === normalize(`static/${this.buildId}/pages/_document.js`))
// If the document chunk can't be found we do nothing // If the document chunk can't be found we do nothing
if (!documentChunk) { if (!documentChunk) {
console.warn('_document.js chunk not found') console.warn('_document.js chunk not found')
@ -209,7 +252,7 @@ export default class HotReloader {
// and to update error content // and to update error content
const failed = failedChunkNames const failed = failedChunkNames
const rootDir = join('bundles', 'pages') const rootDir = join(CLIENT_STATIC_FILES_PATH, this.buildId, 'pages')
for (const n of new Set([...added, ...succeeded, ...removed, ...failed])) { for (const n of new Set([...added, ...succeeded, ...removed, ...failed])) {
const route = toRoute(relative(rootDir, n)) const route = toRoute(relative(rootDir, n))
@ -274,6 +317,7 @@ export default class HotReloader {
const onDemandEntries = onDemandEntryHandler(webpackDevMiddleware, multiCompiler.compilers, { const onDemandEntries = onDemandEntryHandler(webpackDevMiddleware, multiCompiler.compilers, {
dir: this.dir, dir: this.dir,
buildId: this.buildId,
dev: true, dev: true,
reload: this.reload.bind(this), reload: this.reload.bind(this),
pageExtensions: this.config.pageExtensions, pageExtensions: this.config.pageExtensions,

View file

@ -4,18 +4,16 @@ import { parse as parseUrl } from 'url'
import { parse as parseQs } from 'querystring' import { parse as parseQs } from 'querystring'
import fs from 'fs' import fs from 'fs'
import http, { STATUS_CODES } from 'http' import http, { STATUS_CODES } from 'http'
import promisify from '../lib/promisify'
import { import {
renderToHTML, renderToHTML,
renderErrorToHTML, renderErrorToHTML,
sendHTML, sendHTML,
serveStatic, serveStatic
renderScriptError
} from './render' } from './render'
import Router from './router' import Router from './router'
import { isInternalUrl } from './utils' import { isInternalUrl } from './utils'
import loadConfig from './config' import loadConfig from './config'
import {PHASE_PRODUCTION_SERVER, PHASE_DEVELOPMENT_SERVER, BLOCKED_PAGES, BUILD_ID_FILE} from '../lib/constants' import {PHASE_PRODUCTION_SERVER, PHASE_DEVELOPMENT_SERVER, BLOCKED_PAGES, BUILD_ID_FILE, CLIENT_STATIC_FILES_PATH} from '../lib/constants'
import * as asset from '../lib/asset' import * as asset from '../lib/asset'
import * as envConfig from '../lib/runtime-config' import * as envConfig from '../lib/runtime-config'
import { isResSent } from '../lib/utils' import { isResSent } from '../lib/utils'
@ -23,8 +21,6 @@ import { isResSent } from '../lib/utils'
// We need to go up one more level since we are in the `dist` directory // We need to go up one more level since we are in the `dist` directory
import pkg from '../../package' import pkg from '../../package'
const access = promisify(fs.access)
export default class Server { export default class Server {
constructor ({ dir = '.', dev = false, staticMarkup = false, quiet = false, conf = null } = {}) { constructor ({ dir = '.', dev = false, staticMarkup = false, quiet = false, conf = null } = {}) {
this.dir = resolve(dir) this.dir = resolve(dir)
@ -44,8 +40,8 @@ export default class Server {
console.error(`> Could not find a valid build in the '${this.distDir}' directory! Try building your app with 'next build' before starting the server.`) console.error(`> Could not find a valid build in the '${this.distDir}' directory! Try building your app with 'next build' before starting the server.`)
process.exit(1) process.exit(1)
} }
this.buildId = !dev ? this.readBuildId() : '-' this.buildId = this.readBuildId(dev)
this.hotReloader = dev ? this.getHotReloader(this.dir, { quiet, config: this.nextConfig, buildId: this.buildId }) : null this.hotReloader = dev ? this.getHotReloader(this.dir, { config: this.nextConfig, buildId: this.buildId }) : null
this.renderOpts = { this.renderOpts = {
dev, dev,
staticMarkup, staticMarkup,
@ -128,69 +124,19 @@ export default class Server {
async defineRoutes () { async defineRoutes () {
const routes = { const routes = {
'/_next/:buildId/page/:path*.js.map': async (req, res, params) => {
const paths = params.path || ['']
const page = `/${paths.join('/')}`
if (this.dev) {
try {
await this.hotReloader.ensurePage(page)
} catch (err) {
await this.render404(req, res)
}
}
const path = join(this.distDir, 'bundles', 'pages', `${page}.js.map`)
await serveStatic(req, res, path)
},
'/_next/:buildId/page/:path*.js': async (req, res, params) => {
const paths = params.path || ['']
const page = `/${paths.join('/')}`
if (!this.handleBuildId(params.buildId, res)) {
const error = new Error('INVALID_BUILD_ID')
return await renderScriptError(req, res, page, error)
}
if (this.dev && page !== '/_error' && page !== '/_app') {
try {
await this.hotReloader.ensurePage(page)
} catch (error) {
return await renderScriptError(req, res, page, error)
}
const compilationErr = await this.getCompilationError(page)
if (compilationErr) {
return await renderScriptError(req, res, page, compilationErr)
}
}
const p = join(this.distDir, 'bundles', 'pages', `${page}.js`)
// [production] If the page is not exists, we need to send a proper Next.js style 404
// Otherwise, it'll affect the multi-zones feature.
try {
await access(p, (fs.constants || fs).R_OK)
} catch (err) {
return await renderScriptError(req, res, page, { code: 'ENOENT' })
}
await this.serveStatic(req, res, p)
},
'/_next/static/:path*': async (req, res, params) => { '/_next/static/:path*': async (req, res, params) => {
// The commons folder holds commonschunk files // The commons folder holds commonschunk files
// The chunks folder holds dynamic entries // The chunks folder holds dynamic entries
// The buildId folder holds pages and potentially other assets. As buildId changes per build it can be long-term cached.
// In development they don't have a hash, and shouldn't be cached by the browser. // In development they don't have a hash, and shouldn't be cached by the browser.
if (params.path[0] === 'commons' || params.path[0] === 'chunks') { if (params.path[0] === 'commons' || params.path[0] === 'chunks' || params.path[0] === this.buildId) {
if (this.dev) { if (this.dev) {
res.setHeader('Cache-Control', 'no-store, must-revalidate') res.setHeader('Cache-Control', 'no-store, must-revalidate')
} else { } else {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
} }
} }
const p = join(this.distDir, 'static', ...(params.path || [])) const p = join(this.distDir, CLIENT_STATIC_FILES_PATH, ...(params.path || []))
await this.serveStatic(req, res, p) await this.serveStatic(req, res, p)
}, },
@ -269,7 +215,10 @@ export default class Server {
async run (req, res, parsedUrl) { async run (req, res, parsedUrl) {
if (this.hotReloader) { if (this.hotReloader) {
await this.hotReloader.run(req, res) const {finished} = await this.hotReloader.run(req, res, parsedUrl)
if (finished) {
return
}
} }
const fn = this.router.match(req, res, parsedUrl) const fn = this.router.match(req, res, parsedUrl)
@ -392,7 +341,10 @@ export default class Server {
return true return true
} }
readBuildId () { readBuildId (dev) {
if (dev) {
return 'development'
}
const buildIdPath = join(this.distDir, BUILD_ID_FILE) const buildIdPath = join(this.distDir, BUILD_ID_FILE)
const buildId = fs.readFileSync(buildIdPath, 'utf8') const buildId = fs.readFileSync(buildIdPath, 'utf8')
return buildId.trim() return buildId.trim()

View file

@ -17,6 +17,7 @@ const glob = promisify(globModule)
const access = promisify(fs.access) const access = promisify(fs.access)
export default function onDemandEntryHandler (devMiddleware, compilers, { export default function onDemandEntryHandler (devMiddleware, compilers, {
buildId,
dir, dir,
dev, dev,
reload, reload,
@ -163,7 +164,7 @@ export default function onDemandEntryHandler (devMiddleware, compilers, {
const pathname = join(dir, relativePathToPage) const pathname = join(dir, relativePathToPage)
const {name, files} = createEntry(relativePathToPage, {pageExtensions: extensions}) const {name, files} = createEntry(relativePathToPage, {buildId, pageExtensions: extensions})
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const entryInfo = entries[page] const entryInfo = entries[page]

View file

@ -10,10 +10,10 @@ import { loadGetInitialProps, isResSent } from '../lib/utils'
import Head, { defaultHead } from '../lib/head' import Head, { defaultHead } from '../lib/head'
import ErrorDebug from '../lib/error-debug' import ErrorDebug from '../lib/error-debug'
import Loadable from 'react-loadable' import Loadable from 'react-loadable'
import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, SERVER_DIRECTORY } from '../lib/constants' import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH } from '../lib/constants'
// Based on https://github.com/jamiebuilds/react-loadable/pull/132 // Based on https://github.com/jamiebuilds/react-loadable/pull/132
function getBundles (manifest, moduleIds) { function getDynamicImportBundles (manifest, moduleIds) {
return moduleIds.reduce((bundles, moduleId) => { return moduleIds.reduce((bundles, moduleId) => {
if (typeof manifest[moduleId] === 'undefined') { if (typeof manifest[moduleId] === 'undefined') {
return bundles return bundles
@ -62,8 +62,8 @@ async function doRender (req, res, pathname, query, {
await ensurePage(page, { dir, hotReloader }) await ensurePage(page, { dir, hotReloader })
} }
const documentPath = join(distDir, SERVER_DIRECTORY, 'bundles', 'pages', '_document') const documentPath = join(distDir, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH, buildId, 'pages', '_document')
const appPath = join(distDir, SERVER_DIRECTORY, 'bundles', 'pages', '_app') const appPath = join(distDir, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH, buildId, 'pages', '_app')
let [buildManifest, reactLoadableManifest, Component, Document, App] = await Promise.all([ let [buildManifest, reactLoadableManifest, Component, Document, App] = await Promise.all([
require(join(distDir, BUILD_MANIFEST)), require(join(distDir, BUILD_MANIFEST)),
require(join(distDir, REACT_LOADABLE_MANIFEST)), require(join(distDir, REACT_LOADABLE_MANIFEST)),
@ -144,7 +144,7 @@ async function doRender (req, res, pathname, query, {
} }
const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage }) const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage })
const dynamicImports = getBundles(reactLoadableManifest, reactLoadableModules) const dynamicImports = getDynamicImportBundles(reactLoadableManifest, reactLoadableModules)
if (isResSent(res)) return if (isResSent(res)) return

View file

@ -30,22 +30,22 @@ describe('On Demand Entries', () => {
}) })
it('should compile pages for JSON page requests', async () => { it('should compile pages for JSON page requests', async () => {
const pageContent = await renderViaHTTP(context.appPort, '/_next/-/page/about.js') const pageContent = await renderViaHTTP(context.appPort, '/_next/static/development/pages/about.js')
expect(pageContent.includes('About Page')).toBeTruthy() expect(pageContent.includes('About Page')).toBeTruthy()
}) })
it('should dispose inactive pages', async () => { it('should dispose inactive pages', async () => {
const indexPagePath = resolve(__dirname, '../.next/bundles/pages/index.js') const indexPagePath = resolve(__dirname, '../.next/static/development/pages/index.js')
expect(existsSync(indexPagePath)).toBeTruthy() expect(existsSync(indexPagePath)).toBeTruthy()
// Render two pages after the index, since the server keeps at least two pages // Render two pages after the index, since the server keeps at least two pages
await renderViaHTTP(context.appPort, '/about') await renderViaHTTP(context.appPort, '/about')
await renderViaHTTP(context.appPort, '/_next/on-demand-entries-ping', {page: '/about'}) await renderViaHTTP(context.appPort, '/_next/on-demand-entries-ping', {page: '/about'})
const aboutPagePath = resolve(__dirname, '../.next/bundles/pages/about.js') const aboutPagePath = resolve(__dirname, '../.next/static/development/pages/about.js')
await renderViaHTTP(context.appPort, '/third') await renderViaHTTP(context.appPort, '/third')
await renderViaHTTP(context.appPort, '/_next/on-demand-entries-ping', {page: '/third'}) await renderViaHTTP(context.appPort, '/_next/on-demand-entries-ping', {page: '/third'})
const thirdPagePath = resolve(__dirname, '../.next/bundles/pages/third.js') const thirdPagePath = resolve(__dirname, '../.next/static/development/pages/third.js')
// Wait maximum of jasmine.DEFAULT_TIMEOUT_INTERVAL checking // Wait maximum of jasmine.DEFAULT_TIMEOUT_INTERVAL checking
// for disposing /about // for disposing /about

View file

@ -63,7 +63,7 @@ describe('Production Usage', () => {
const resources = [] const resources = []
// test a regular page // test a regular page
resources.push(`${url}${buildId}/page/index.js`) resources.push(`${url}static/${buildId}/pages/index.js`)
// test dynamic chunk // test dynamic chunk
resources.push(url + reactLoadableManifest['../../components/hello1'][0].publicPath) resources.push(url + reactLoadableManifest['../../components/hello1'][0].publicPath)

View file

@ -1,6 +1,6 @@
{ {
"/index": "bundles/pages/index.js", "/index": "static/development/pages/index.js",
"/world": "bundles/pages/world.js", "/world": "static/development/pages/world.js",
"/_error": "bundles/pages/_error.js", "/_error": "static/development/pages/_error.js",
"/non-existent-child": "bundles/pages/non-existent-child.js" "/non-existent-child": "static/development/pages/non-existent-child.js"
} }

View file

@ -1,12 +1,12 @@
/* global describe, it, expect */ /* global describe, it, expect */
import { join } from 'path' import { join } from 'path'
import {SERVER_DIRECTORY} from 'next/constants' import {SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH} from 'next/constants'
import requirePage, {getPagePath, normalizePagePath, pageNotFoundError} from '../../dist/server/require' import requirePage, {getPagePath, normalizePagePath, pageNotFoundError} from '../../dist/server/require'
const sep = '/' const sep = '/'
const distDir = join(__dirname, '_resolvedata') const distDir = join(__dirname, '_resolvedata')
const pathToBundles = join(distDir, SERVER_DIRECTORY, 'bundles', 'pages') const pathToBundles = join(distDir, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH, 'development', 'pages')
describe('pageNotFoundError', () => { describe('pageNotFoundError', () => {
it('Should throw error with ENOENT code', () => { it('Should throw error with ENOENT code', () => {

View file

@ -3,46 +3,54 @@
import {normalize, join} from 'path' import {normalize, join} from 'path'
import {getPageEntries, createEntry} from '../../dist/build/webpack/utils' import {getPageEntries, createEntry} from '../../dist/build/webpack/utils'
const buildId = 'development'
describe('createEntry', () => { describe('createEntry', () => {
it('Should turn a path into a page entry', () => { it('Should turn a path into a page entry', () => {
const entry = createEntry('pages/index.js') const entry = createEntry('pages/index.js')
expect(entry.name).toBe(normalize('bundles/pages/index.js')) expect(entry.name).toBe(normalize(`static/pages/index.js`))
expect(entry.files[0]).toBe('./pages/index.js') expect(entry.files[0]).toBe('./pages/index.js')
}) })
it('Should have a custom name', () => { it('Should have a custom name', () => {
const entry = createEntry('pages/index.js', {name: 'something-else.js'}) const entry = createEntry('pages/index.js', {name: 'something-else.js'})
expect(entry.name).toBe(normalize('bundles/something-else.js')) expect(entry.name).toBe(normalize(`static/something-else.js`))
expect(entry.files[0]).toBe('./pages/index.js') expect(entry.files[0]).toBe('./pages/index.js')
}) })
it('Should allow custom extension like .ts to be turned into .js', () => { it('Should allow custom extension like .ts to be turned into .js', () => {
const entry = createEntry('pages/index.ts', {pageExtensions: ['js', 'ts'].join('|')}) const entry = createEntry('pages/index.ts', {pageExtensions: ['js', 'ts'].join('|')})
expect(entry.name).toBe(normalize('bundles/pages/index.js')) expect(entry.name).toBe(normalize('static/pages/index.js'))
expect(entry.files[0]).toBe('./pages/index.ts') expect(entry.files[0]).toBe('./pages/index.ts')
}) })
it('Should allow custom extension like .jsx to be turned into .js', () => { it('Should allow custom extension like .jsx to be turned into .js', () => {
const entry = createEntry('pages/index.jsx', {pageExtensions: ['jsx', 'js'].join('|')}) const entry = createEntry('pages/index.jsx', {pageExtensions: ['jsx', 'js'].join('|')})
expect(entry.name).toBe(normalize('bundles/pages/index.js')) expect(entry.name).toBe(normalize('static/pages/index.js'))
expect(entry.files[0]).toBe('./pages/index.jsx') expect(entry.files[0]).toBe('./pages/index.jsx')
}) })
it('Should allow custom extension like .tsx to be turned into .js', () => { it('Should allow custom extension like .tsx to be turned into .js', () => {
const entry = createEntry('pages/index.tsx', {pageExtensions: ['tsx', 'ts'].join('|')}) const entry = createEntry('pages/index.tsx', {pageExtensions: ['tsx', 'ts'].join('|')})
expect(entry.name).toBe(normalize('bundles/pages/index.js')) expect(entry.name).toBe(normalize('static/pages/index.js'))
expect(entry.files[0]).toBe('./pages/index.tsx') expect(entry.files[0]).toBe('./pages/index.tsx')
}) })
it('Should allow custom extension like .tsx to be turned into .js with another order', () => { it('Should allow custom extension like .tsx to be turned into .js with another order', () => {
const entry = createEntry('pages/index.tsx', {pageExtensions: ['ts', 'tsx'].join('|')}) const entry = createEntry('pages/index.tsx', {pageExtensions: ['ts', 'tsx'].join('|')})
expect(entry.name).toBe(normalize('bundles/pages/index.js')) expect(entry.name).toBe(normalize('static/pages/index.js'))
expect(entry.files[0]).toBe('./pages/index.tsx') expect(entry.files[0]).toBe('./pages/index.tsx')
}) })
it('Should turn pages/blog/index.js into pages/blog.js', () => { it('Should turn pages/blog/index.js into pages/blog.js', () => {
const entry = createEntry('pages/blog/index.js') const entry = createEntry('pages/blog/index.js')
expect(entry.name).toBe(normalize('bundles/pages/blog.js')) expect(entry.name).toBe(normalize('static/pages/blog.js'))
expect(entry.files[0]).toBe('./pages/blog/index.js')
})
it('Should add buildId when provided', () => {
const entry = createEntry('pages/blog/index.js', {buildId})
expect(entry.name).toBe(normalize(`static/${buildId}/pages/blog.js`))
expect(entry.files[0]).toBe('./pages/blog/index.js') expect(entry.files[0]).toBe('./pages/blog/index.js')
}) })
}) })
@ -53,30 +61,30 @@ describe('getPageEntries', () => {
it('Should return paths', () => { it('Should return paths', () => {
const pagePaths = ['pages/index.js'] const pagePaths = ['pages/index.js']
const pageEntries = getPageEntries(pagePaths, {nextPagesDir}) const pageEntries = getPageEntries(pagePaths, {nextPagesDir})
expect(pageEntries[normalize('bundles/pages/index.js')][0]).toBe('./pages/index.js') expect(pageEntries[normalize('static/pages/index.js')][0]).toBe('./pages/index.js')
}) })
it('Should include default _error', () => { it('Should include default _error', () => {
const pagePaths = ['pages/index.js'] const pagePaths = ['pages/index.js']
const pageEntries = getPageEntries(pagePaths, {nextPagesDir}) const pageEntries = getPageEntries(pagePaths, {nextPagesDir})
expect(pageEntries[normalize('bundles/pages/_error.js')][0]).toMatch(/dist[/\\]pages[/\\]_error\.js/) expect(pageEntries[normalize('static/pages/_error.js')][0]).toMatch(/dist[/\\]pages[/\\]_error\.js/)
}) })
it('Should not include default _error when _error.js is inside the pages directory', () => { it('Should not include default _error when _error.js is inside the pages directory', () => {
const pagePaths = ['pages/index.js', 'pages/_error.js'] const pagePaths = ['pages/index.js', 'pages/_error.js']
const pageEntries = getPageEntries(pagePaths, {nextPagesDir}) const pageEntries = getPageEntries(pagePaths, {nextPagesDir})
expect(pageEntries[normalize('bundles/pages/_error.js')][0]).toBe('./pages/_error.js') expect(pageEntries[normalize('static/pages/_error.js')][0]).toBe('./pages/_error.js')
}) })
it('Should include default _document when isServer is true', () => { it('Should include default _document when isServer is true', () => {
const pagePaths = ['pages/index.js'] const pagePaths = ['pages/index.js']
const pageEntries = getPageEntries(pagePaths, {nextPagesDir, isServer: true}) const pageEntries = getPageEntries(pagePaths, {nextPagesDir, isServer: true})
expect(pageEntries[normalize('bundles/pages/_document.js')][0]).toMatch(/dist[/\\]pages[/\\]_document\.js/) expect(pageEntries[normalize('static/pages/_document.js')][0]).toMatch(/dist[/\\]pages[/\\]_document\.js/)
}) })
it('Should not include default _document when _document.js is inside the pages directory', () => { it('Should not include default _document when _document.js is inside the pages directory', () => {
const pagePaths = ['pages/index.js', 'pages/_document.js'] const pagePaths = ['pages/index.js', 'pages/_document.js']
const pageEntries = getPageEntries(pagePaths, {nextPagesDir, isServer: true}) const pageEntries = getPageEntries(pagePaths, {nextPagesDir, isServer: true})
expect(pageEntries[normalize('bundles/pages/_document.js')][0]).toBe('./pages/_document.js') expect(pageEntries[normalize('static/pages/_document.js')][0]).toBe('./pages/_document.js')
}) })
}) })

View file

@ -3352,10 +3352,6 @@ foreach@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
foreachasync@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6"
foreground-child@^1.5.3, foreground-child@^1.5.6: foreground-child@^1.5.3, foreground-child@^1.5.6:
version "1.5.6" version "1.5.6"
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-1.5.6.tgz#4fd71ad2dfde96789b980a5c0a295937cb2f5ce9" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-1.5.6.tgz#4fd71ad2dfde96789b980a5c0a295937cb2f5ce9"
@ -7651,12 +7647,6 @@ vm-browserify@0.0.4:
dependencies: dependencies:
indexof "0.0.1" indexof "0.0.1"
walk@2.3.9:
version "2.3.9"
resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.9.tgz#31b4db6678f2ae01c39ea9fb8725a9031e558a7b"
dependencies:
foreachasync "^3.0.0"
walkdir@^0.0.11: walkdir@^0.0.11:
version "0.0.11" version "0.0.11"
resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.0.11.tgz#a16d025eb931bd03b52f308caed0f40fcebe9532" resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.0.11.tgz#a16d025eb931bd03b52f308caed0f40fcebe9532"