Skip to content

Commit cdd54af

Browse files
ijjkTimer
authored andcommitted
Add auto static/dynamic (#7293)
* Add automatic exporting of pages with no getInitialProps * Add support for exporting serverless to static and serving the html files during next start * Fix missing runtimeEnv when requiring page, re-add warning when trying to export with serverless, and update tests * Update flying-shuttle test * revert un-used pagesManifest change * remove query.amp RegExp test * Fix windows backslashes not being replaced * Re-enable serverless support for next start * bump * Fix getInitialProps check * Fix incorrect error check * Re-add check for reserved pages * Fix static check * Update to ignore /api pages and clean up some tests * Re-add needed next.config for test and correct behavior * Update RegExp for ignored pages for auto-static * Add checking for custom getInitialProps in pages/_app * Update isPageStatic logic to only use default export * Re-add retrying to CircleCi * Update query during dev to only have values available during export for static pages * Fix test * Add warning when page without default export is found and make sure to update pages-manifest correctly in flying-shuttle mode * Fix backslashes not being replaced * Integrate auto-static with flying-shuttle and make sure AMP is handled in flying-shuttle * Add autoExport for opting in
1 parent 4718bd6 commit cdd54af

File tree

37 files changed

+515
-196
lines changed

37 files changed

+515
-196
lines changed

packages/next-server/lib/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const BUILD_MANIFEST = 'build-manifest.json'
77
export const REACT_LOADABLE_MANIFEST = 'react-loadable-manifest.json'
88
export const CHUNK_GRAPH_MANIFEST = 'compilation-modules.json'
99
export const SERVER_DIRECTORY = 'server'
10+
export const SERVERLESS_DIRECTORY = 'serverless'
1011
export const CONFIG_FILE = 'next.config.js'
1112
export const BUILD_ID_FILE = 'BUILD_ID'
1213
export const BLOCKED_PAGES = [
@@ -27,4 +28,5 @@ export const CLIENT_STATIC_FILES_RUNTIME_WEBPACK = `${CLIENT_STATIC_FILES_RUNTIM
2728
export const IS_BUNDLED_PAGE_REGEX = /^static[/\\][^/\\]+[/\\]pages.*\.js$/
2829
// matches static/<buildid>/pages/:page*.js
2930
export const ROUTE_NAME_REGEX = /^static[/\\][^/\\]+[/\\]pages[/\\](.*)\.js$/
31+
export const SERVERLESS_ROUTE_NAME_REGEX = /^pages[/\\](.*)\.js$/
3032
export const HEAD_BUILD_ID_FILE = `${CLIENT_STATIC_FILES_PATH}/HEAD_BUILD_ID`

packages/next-server/server/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const defaultConfig: {[key: string]: any} = {
2727
(Number(process.env.CIRCLE_NODE_TOTAL) ||
2828
(os.cpus() || { length: 1 }).length) - 1,
2929
),
30+
autoExport: false,
3031
ampBindInitData: false,
3132
exportTrailingSlash: true,
3233
terserLoader: false,

packages/next-server/server/load-components.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {BUILD_MANIFEST, CLIENT_STATIC_FILES_PATH, REACT_LOADABLE_MANIFEST, SERVER_DIRECTORY} from '../lib/constants';
1+
import {BUILD_MANIFEST, CLIENT_STATIC_FILES_PATH, REACT_LOADABLE_MANIFEST, SERVER_DIRECTORY, SERVERLESS_DIRECTORY} from '../lib/constants';
22
import { join } from 'path';
33

44
import { requirePage } from './require';
@@ -7,14 +7,18 @@ export function interopDefault(mod: any) {
77
return mod.default || mod
88
}
99

10-
export async function loadComponents(distDir: string, buildId: string, pathname: string) {
10+
export async function loadComponents(distDir: string, buildId: string, pathname: string, serverless: boolean) {
11+
if (serverless) {
12+
const Component = await requirePage(pathname, distDir, serverless)
13+
return { Component }
14+
}
1115
const documentPath = join(distDir, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH, buildId, 'pages', '_document')
1216
const appPath = join(distDir, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH, buildId, 'pages', '_app')
1317

1418
const [buildManifest, reactLoadableManifest, Component, Document, App] = await Promise.all([
1519
require(join(distDir, BUILD_MANIFEST)),
1620
require(join(distDir, REACT_LOADABLE_MANIFEST)),
17-
interopDefault(requirePage(pathname, distDir)),
21+
interopDefault(requirePage(pathname, distDir, serverless)),
1822
interopDefault(require(documentPath)),
1923
interopDefault(require(appPath)),
2024
])

packages/next-server/server/next-server.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,8 @@ export default class Server {
7272
publicRuntimeConfig,
7373
assetPrefix,
7474
generateEtags,
75-
target,
7675
} = this.nextConfig
7776

78-
if (process.env.NODE_ENV === 'production' && target !== 'server')
79-
throw new Error(
80-
'Cannot start server when target is not server. https://err.sh/zeit/next.js/next-start-serverless',
81-
)
82-
8377
this.buildId = this.readBuildId()
8478
this.renderOpts = {
8579
ampBindInitData: this.nextConfig.experimental.ampBindInitData,
@@ -257,7 +251,7 @@ export default class Server {
257251
* @param pathname path of request
258252
*/
259253
private resolveApiRequest(pathname: string) {
260-
return getPagePath(pathname, this.distDir)
254+
return getPagePath(pathname, this.distDir, this.nextConfig.target === 'serverless')
261255
}
262256

263257
private generatePublicRoutes(): Route[] {
@@ -353,7 +347,25 @@ export default class Server {
353347
query: ParsedUrlQuery = {},
354348
opts: any,
355349
) {
356-
const result = await loadComponents(this.distDir, this.buildId, pathname)
350+
const serverless = this.nextConfig.target === 'serverless'
351+
// try serving a static AMP version first
352+
if (query.amp) {
353+
try {
354+
const result = await loadComponents(this.distDir, this.buildId, (pathname === '/' ? '/index' : pathname) + '.amp', serverless)
355+
if (typeof result.Component === 'string') return result.Component
356+
} catch (err) {
357+
if (err.code !== 'ENOENT') throw err
358+
}
359+
}
360+
const result = await loadComponents(this.distDir, this.buildId, pathname, serverless)
361+
// handle static page
362+
if (typeof result.Component === 'string') return result.Component
363+
// handle serverless
364+
if (typeof result.Component === 'object' &&
365+
typeof result.Component.renderReqToHTML === 'function'
366+
) {
367+
return result.Component.renderReqToHTML(req, res)
368+
}
357369
return renderToHTML(req, res, pathname, query, { ...result, ...opts })
358370
}
359371

packages/next-server/server/render.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,16 @@ export async function renderToHTML(
252252
`The default export is not a React Component in page: "/_document"`,
253253
)
254254
}
255+
256+
const isStaticPage = typeof (Component as any).getInitialProps !== 'function'
257+
const defaultAppGetInitialProps = App.getInitialProps === (App as any).origGetInitialProps
258+
259+
if (isStaticPage && defaultAppGetInitialProps) {
260+
// remove query values except ones that will be set during export
261+
query = {
262+
amp: query.amp,
263+
}
264+
}
255265
}
256266

257267
// @ts-ignore url will always be set
@@ -304,7 +314,7 @@ export async function renderToHTML(
304314

305315
const ampMode = {
306316
enabled: false,
307-
hasQuery: Boolean(query.amp && /^(y|yes|true|1)/i.test(query.amp.toString())),
317+
hasQuery: Boolean(query.amp),
308318
}
309319

310320
if (ampBindInitData) {

packages/next-server/server/require.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1+
import fs from 'fs'
12
import {join} from 'path'
2-
import {PAGES_MANIFEST, SERVER_DIRECTORY} from '../lib/constants'
3+
import {promisify} from 'util'
4+
import {PAGES_MANIFEST, SERVER_DIRECTORY, SERVERLESS_DIRECTORY} from '../lib/constants'
35
import { normalizePagePath } from './normalize-page-path'
46

7+
const readFile = promisify(fs.readFile)
8+
59
export function pageNotFoundError(page: string): Error {
610
const err: any = new Error(`Cannot find module for page: ${page}`)
711
err.code = 'ENOENT'
812
return err
913
}
1014

11-
export function getPagePath(page: string, distDir: string): string {
12-
const serverBuildPath = join(distDir, SERVER_DIRECTORY)
15+
export function getPagePath(page: string, distDir: string, serverless: boolean): string {
16+
const serverBuildPath = join(distDir, serverless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY)
1317
const pagesManifest = require(join(serverBuildPath, PAGES_MANIFEST))
1418

1519
try {
@@ -32,7 +36,10 @@ export function getPagePath(page: string, distDir: string): string {
3236
return join(serverBuildPath, pagesManifest[page])
3337
}
3438

35-
export function requirePage(page: string, distDir: string): any {
36-
const pagePath = getPagePath(page, distDir)
39+
export function requirePage(page: string, distDir: string, serverless: boolean): any {
40+
const pagePath = getPagePath(page, distDir, serverless)
41+
if (pagePath.endsWith('.html')) {
42+
return readFile(pagePath, 'utf8')
43+
}
3744
return require(pagePath)
3845
}

packages/next/build/flying-shuttle.ts

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { promisify } from 'util'
99

1010
import { recursiveDelete } from '../lib/recursive-delete'
1111
import * as Log from './output/log'
12+
import { PageInfo } from './utils';
1213

1314
const FILE_BUILD_ID = 'HEAD_BUILD_ID'
1415
const FILE_UPDATED_AT = 'UPDATED_AT'
@@ -220,6 +221,29 @@ export class FlyingShuttle {
220221
return (this._shuttleBuildId = contents)
221222
}
222223

224+
getPageInfos = async (): Promise<Map<string, PageInfo>> => {
225+
const pageInfos: Map<string, PageInfo> = new Map()
226+
const pagesManifest = JSON.parse(await fsReadFile(
227+
path.join(
228+
this.shuttleDirectory, DIR_FILES_NAME, 'serverless/pages-manifest.json'
229+
),
230+
'utf8'
231+
))
232+
Object.keys(pagesManifest).forEach(pg => {
233+
const path = pagesManifest[pg]
234+
const isStatic: boolean = path.endsWith('html')
235+
let isAmp = Boolean(pagesManifest[pg + '.amp'])
236+
if (pg === '/') isAmp = Boolean(pagesManifest['/index.amp'])
237+
pageInfos.set(pg, {
238+
isAmp,
239+
size: 0,
240+
static: isStatic,
241+
serverBundle: path
242+
})
243+
})
244+
return pageInfos
245+
}
246+
223247
getUnchangedPages = async () => {
224248
const manifestPath = path.join(this.shuttleDirectory, CHUNK_GRAPH_MANIFEST)
225249
const manifest = require(manifestPath) as ChunkGraphManifest
@@ -276,8 +300,36 @@ export class FlyingShuttle {
276300
return unchangedPages
277301
}
278302

279-
restorePage = async (page: string): Promise<boolean> => {
303+
mergePagesManifest = async (): Promise<void> => {
304+
const savedPagesManifest = path.join(
305+
this.shuttleDirectory, DIR_FILES_NAME, 'serverless/pages-manifest.json'
306+
)
307+
if (!(await fsExists(savedPagesManifest))) return
308+
309+
const saved = JSON.parse(await fsReadFile(
310+
savedPagesManifest,
311+
'utf8'
312+
))
313+
const currentPagesManifest = path.join(
314+
this.distDirectory, 'serverless/pages-manifest.json'
315+
)
316+
const current = JSON.parse(await fsReadFile(
317+
currentPagesManifest,
318+
'utf8'
319+
))
320+
321+
await fsWriteFile(currentPagesManifest, JSON.stringify({
322+
...saved,
323+
...current,
324+
}))
325+
}
326+
327+
restorePage = async (
328+
page: string,
329+
pageInfo: PageInfo = {} as PageInfo
330+
): Promise<boolean> => {
280331
await this._restoreSema.acquire()
332+
281333
try {
282334
const manifestPath = path.join(
283335
this.shuttleDirectory,
@@ -293,10 +345,9 @@ export class FlyingShuttle {
293345

294346
const serverless = path.join(
295347
'serverless/pages',
296-
`${page === '/' ? 'index' : page}.js`
348+
`${page === '/' ? 'index' : page}.${pageInfo.static ? 'html' : 'js'}`
297349
)
298350
const files = [serverless, ...pageChunks[page]]
299-
300351
const filesExists = await Promise.all(
301352
files
302353
.map(f => path.join(this.shuttleDirectory, DIR_FILES_NAME, f))
@@ -366,7 +417,7 @@ export class FlyingShuttle {
366417
}
367418
}
368419

369-
save = async () => {
420+
save = async (staticPages: Set<string>, pageInfos: Map<string, PageInfo>) => {
370421
Log.wait('docking flying shuttle')
371422

372423
await recursiveDelete(this.shuttleDirectory)
@@ -419,10 +470,30 @@ export class FlyingShuttle {
419470
const usedChunks = new Set()
420471
const pages = Object.keys(storeManifest.pageChunks)
421472
pages.forEach(page => {
422-
storeManifest.pageChunks[page].forEach(file => usedChunks.add(file))
473+
const info = pageInfos.get(page) || {} as PageInfo
474+
475+
storeManifest.pageChunks[page].forEach((file, idx) => {
476+
if (info.isAmp) {
477+
// AMP pages don't have client bundles
478+
storeManifest.pageChunks[page] = []
479+
return
480+
}
481+
usedChunks.add(file)
482+
})
423483
usedChunks.add(
424-
path.join('serverless/pages', `${page === '/' ? 'index' : page}.js`)
484+
path.join('serverless/pages', `${
485+
page === '/' ? 'index' : page
486+
}.${staticPages.has(page) ? 'html' : 'js'}`)
425487
)
488+
const ampPage = (page === '/' ? '/index' : page) + '.amp'
489+
490+
if (staticPages.has(ampPage)) {
491+
storeManifest.pages[ampPage] = []
492+
storeManifest.pageChunks[ampPage] = []
493+
usedChunks.add(
494+
path.join('serverless/pages', `${ampPage}.html`)
495+
)
496+
}
426497
})
427498

428499
await fsWriteFile(
@@ -442,6 +513,11 @@ export class FlyingShuttle {
442513
})
443514
)
444515

516+
await fsCopyFile(
517+
path.join(this.distDirectory, 'serverless/pages-manifest.json'),
518+
path.join(this.shuttleDirectory, DIR_FILES_NAME, 'serverless/pages-manifest.json')
519+
)
520+
445521
Log.info(`flying shuttle payload: ${usedChunks.size + 2} files`)
446522
Log.ready('flying shuttle docked')
447523

0 commit comments

Comments
 (0)