Skip to content

Commit

Permalink
Make Client request BUILD_ID from the Server (vercel#6891)
Browse files Browse the repository at this point in the history
* Generate two versions of pages

* Add code VSCode deleted

* Add dynamicBuildId option to __NEXT_DATA__

* Reduce amount of diff

* Make getPageFile code easier to read

* Minimize diff

* minimize diff

* Fix default value for dynamicBuildId

* Fix weird bug

* Fetch the head build id on client

* Move __selectivePageBuilding

* Add tests

* Remove _this

* Add console warning
  • Loading branch information
Timer authored Apr 4, 2019
1 parent 9225eb8 commit 19c6351
Show file tree
Hide file tree
Showing 22 changed files with 294 additions and 47 deletions.
10 changes: 10 additions & 0 deletions errors/head-build-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Failed to load `BUILD_ID` from Server

#### Why This Error Occurred

The deployment was generated incorrectly or the server was overloaded at the time of the request.

#### Possible Ways to Fix It

Please make sure you are using the latest version of the `@now/next` builder in your `now.json`.
If this error persists, please file a bug report.
1 change: 1 addition & 0 deletions packages/next-server/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export const CLIENT_STATIC_FILES_RUNTIME_WEBPACK = `${CLIENT_STATIC_FILES_RUNTIM
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 HEAD_BUILD_ID_FILE = `${CLIENT_STATIC_FILES_PATH}/HEAD_BUILD_ID`
3 changes: 3 additions & 0 deletions packages/next-server/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ type RenderOpts = {
ampBindInitData: boolean
staticMarkup: boolean
buildId: string
dynamicBuildId?: boolean
runtimeConfig?: { [key: string]: any }
assetPrefix?: string
err?: Error | null
Expand Down Expand Up @@ -143,6 +144,7 @@ function renderDocument(
pathname,
query,
buildId,
dynamicBuildId = false,
assetPrefix,
runtimeConfig,
nextExport,
Expand Down Expand Up @@ -182,6 +184,7 @@ function renderDocument(
page: pathname, // The rendered page
query, // querystring parsed / passed by the user
buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
dynamicBuildId, // Specifies if the buildId should by dynamically fetched
assetPrefix: assetPrefix === '' ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML
runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML
nextExport, // If this is a page exported by `next export`
Expand Down
5 changes: 3 additions & 2 deletions packages/next/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type Entrypoints = {
server: WebpackEntrypoints
}

export function createEntrypoints(pages: PagesMapping, target: 'server'|'serverless', buildId: string, config: any): Entrypoints {
export function createEntrypoints(pages: PagesMapping, target: 'server'|'serverless', buildId: string, dynamicBuildId: boolean, config: any): Entrypoints {
const client: WebpackEntrypoints = {}
const server: WebpackEntrypoints = {}

Expand All @@ -44,7 +44,8 @@ export function createEntrypoints(pages: PagesMapping, target: 'server'|'serverl
assetPrefix: config.assetPrefix,
generateEtags: config.generateEtags,
ampEnabled: config.experimental.amp,
ampBindInitData: config.experimental.ampBindInitData
ampBindInitData: config.experimental.ampBindInitData,
dynamicBuildId
}

Object.keys(pages).forEach((page) => {
Expand Down
11 changes: 7 additions & 4 deletions packages/next/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ export default async function build(
? [process.env.__NEXT_BUILDER_EXPERIMENTAL_PAGE]
: []

const __selectivePageBuilding = pages ? Boolean(pages.length) : false

let pagePaths
if (pages && pages.length) {
if (__selectivePageBuilding) {
if (config.target !== 'serverless') {
throw new Error(
'Cannot use selective page building without the serverless target.'
Expand Down Expand Up @@ -125,6 +127,7 @@ export default async function build(
mappedPages,
config.target,
buildId,
__selectivePageBuilding,
config
)
const configs = await Promise.all([
Expand All @@ -135,7 +138,7 @@ export default async function build(
config,
target: config.target,
entrypoints: entrypoints.client,
__selectivePageBuilding: pages && Boolean(pages.length),
__selectivePageBuilding,
}),
getBaseWebpackConfig(dir, {
debug,
Expand All @@ -144,7 +147,7 @@ export default async function build(
config,
target: config.target,
entrypoints: entrypoints.server,
__selectivePageBuilding: pages && Boolean(pages.length),
__selectivePageBuilding,
}),
])

Expand Down Expand Up @@ -203,5 +206,5 @@ export default async function build(

printTreeView(Object.keys(mappedPages))

await writeBuildId(distDir, buildId)
await writeBuildId(distDir, buildId, __selectivePageBuilding)
}
2 changes: 1 addition & 1 deletion packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ export default function getBaseWebpackConfig (dir: string, {dev = false, debug =
return /next-server[\\/]dist[\\/]/.test(context) || /next[\\/]dist[\\/]/.test(context)
}
}),
target === 'serverless' && isServer && new ServerlessPlugin(buildId),
target === 'serverless' && (isServer || __selectivePageBuilding) && new ServerlessPlugin(buildId, { isServer }),
target !== 'serverless' && isServer && new PagesManifestPlugin(),
target !== 'serverless' && isServer && new NextJsSSRModuleCachePlugin({ outputPath }),
isServer && new NextJsSsrImportPlugin(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type ServerlessLoaderQuery = {
ampEnabled: boolean | string,
ampBindInitData: boolean | string,
generateEtags: string
dynamicBuildId?: string | boolean
}

const nextServerlessLoader: loader.Loader = function () {
Expand All @@ -27,7 +28,8 @@ const nextServerlessLoader: loader.Loader = function () {
absoluteAppPath,
absoluteDocumentPath,
absoluteErrorPath,
generateEtags
generateEtags,
dynamicBuildId
}: ServerlessLoaderQuery = typeof this.query === 'string' ? parse(this.query.substr(1)) : this.query
const buildManifest = join(distDir, BUILD_MANIFEST).replace(/\\/g, '/')
const reactLoadableManifest = join(distDir, REACT_LOADABLE_MANIFEST).replace(/\\/g, '/')
Expand All @@ -48,6 +50,7 @@ const nextServerlessLoader: loader.Loader = function () {
buildManifest,
reactLoadableManifest,
buildId: "__NEXT_REPLACE__BUILD_ID__",
dynamicBuildId: ${dynamicBuildId === true || dynamicBuildId === 'true'},
assetPrefix: "${assetPrefix}",
ampEnabled: ${ampEnabled === true || ampEnabled === 'true'},
ampBindInitData: ${ampBindInitData === true || ampBindInitData === 'true'}
Expand Down
54 changes: 37 additions & 17 deletions packages/next/build/webpack/plugins/serverless-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,32 +53,52 @@ function interceptFileWrites(

export class ServerlessPlugin {
private buildId: string
private isServer: boolean

constructor(buildId: string) {
constructor(buildId: string, { isServer = false } = {}) {
this.buildId = buildId
this.isServer = isServer
}

apply(compiler: Compiler) {
interceptFileWrites(compiler, content =>
replaceInBuffer(content, NEXT_REPLACE_BUILD_ID, this.buildId)
)
if (this.isServer) {
interceptFileWrites(compiler, content =>
replaceInBuffer(content, NEXT_REPLACE_BUILD_ID, this.buildId)
)

compiler.hooks.compilation.tap('ServerlessPlugin', compilation => {
compilation.hooks.optimizeChunksBasic.tap('ServerlessPlugin', chunks => {
chunks.forEach(chunk => {
// If chunk is not an entry point skip them
if (chunk.hasEntryModule()) {
const dynamicChunks = chunk.getAllAsyncChunks()
if (dynamicChunks.size !== 0) {
for (const dynamicChunk of dynamicChunks) {
for (const module of dynamicChunk.modulesIterable) {
GraphHelpers.connectChunkAndModule(chunk, module)
compiler.hooks.compilation.tap('ServerlessPlugin', compilation => {
compilation.hooks.optimizeChunksBasic.tap(
'ServerlessPlugin',
chunks => {
chunks.forEach(chunk => {
// If chunk is not an entry point skip them
if (chunk.hasEntryModule()) {
const dynamicChunks = chunk.getAllAsyncChunks()
if (dynamicChunks.size !== 0) {
for (const dynamicChunk of dynamicChunks) {
for (const module of dynamicChunk.modulesIterable) {
GraphHelpers.connectChunkAndModule(chunk, module)
}
}
}
}
}
})
}
})
)
})
} else {
compiler.hooks.emit.tap('ServerlessPlugin', compilation => {
const assetNames = Object.keys(compilation.assets).filter(f =>
f.includes(this.buildId)
)
for (const name of assetNames) {
compilation.assets[
name
.replace(new RegExp(`${this.buildId}[\\/\\\\]`), '')
.replace(/[.]js$/, `.${this.buildId}.js`)
] = compilation.assets[name]
}
})
})
}
}
}
9 changes: 7 additions & 2 deletions packages/next/build/write-build-id.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import fs from 'fs'
import {promisify} from 'util'
import {join} from 'path'
import {BUILD_ID_FILE} from 'next-server/constants'
import {BUILD_ID_FILE, HEAD_BUILD_ID_FILE} from 'next-server/constants'

const writeFile = promisify(fs.writeFile)

export async function writeBuildId (distDir: string, buildId: string): Promise<void> {
export async function writeBuildId (distDir: string, buildId: string, headBuildId: boolean): Promise<void> {
const buildIdPath = join(distDir, BUILD_ID_FILE)
await writeFile(buildIdPath, buildId, 'utf8')

if (headBuildId) {
const headBuildIdPath = join(distDir, HEAD_BUILD_ID_FILE)
await writeFile(headBuildIdPath, buildId, 'utf8')
}
}
5 changes: 5 additions & 0 deletions packages/next/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const {
page,
query,
buildId,
dynamicBuildId,
assetPrefix,
runtimeConfig,
dynamicIds
Expand Down Expand Up @@ -99,6 +100,10 @@ export default async ({

await Loadable.preloadReady(dynamicIds || [])

if (dynamicBuildId === true) {
pageLoader.onDynamicBuildId()
}

router = createRouter(page, query, asPath, {
initialProps: props,
pageLoader,
Expand Down
37 changes: 36 additions & 1 deletion packages/next/client/page-loader.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* global document */
import mitt from 'next-server/dist/lib/mitt'
import unfetch from 'unfetch'

// smaller version of https://gist.github.com/igrigorik/a02f2359f3bc50ca7a9c
function supportsPreload (list) {
Expand All @@ -24,6 +25,7 @@ export default class PageLoader {
this.prefetchCache = new Set()
this.pageRegisterEvents = mitt()
this.loadingRoutes = {}
this.promisedBuildId = Promise.resolve()
}

normalizeRoute (route) {
Expand Down Expand Up @@ -76,7 +78,38 @@ export default class PageLoader {
})
}

loadScript (route) {
onDynamicBuildId () {
this.promisedBuildId = new Promise(resolve => {
unfetch(`${this.assetPrefix}/_next/static/HEAD_BUILD_ID`)
.then(res => {
if (res.ok) {
return res
}

const err = new Error('Failed to fetch HEAD buildId')
err.res = res
throw err
})
.then(res => res.text())
.then(buildId => {
this.buildId = buildId.trim()
})
.catch(() => {
// When this fails it's not a _huge_ deal, preload wont work and page
// navigation will 404, triggering a SSR refresh
console.warn(
'Failed to load BUILD_ID from server. ' +
'The following client-side page transition will likely 404 and cause a SSR.\n' +
'http://err.sh/zeit/next.js/head-build-id'
)
})
.then(resolve, resolve)
})
}

async loadScript (route) {
await this.promisedBuildId

route = this.normalizeRoute(route)
const scriptRoute = route === '/' ? '/index.js' : `${route}.js`

Expand Down Expand Up @@ -146,6 +179,8 @@ export default class PageLoader {
// If not fall back to loading script tags before the page is loaded
// https://caniuse.com/#feat=link-rel-preload
if (hasPreload) {
await this.promisedBuildId

const link = document.createElement('link')
link.rel = 'preload'
link.crossOrigin = process.crossOrigin
Expand Down
2 changes: 1 addition & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
"@types/webpack-sources": "0.1.5",
"@zeit/ncc": "0.15.2",
"arg": "4.1.0",
"nanoid": "1.2.1",
"nanoid": "2.0.1",
"resolve": "1.5.0",
"taskr": "1.1.0",
"text-table": "0.2.0",
Expand Down
Loading

0 comments on commit 19c6351

Please sign in to comment.