Skip to content

Commit 912838d

Browse files
Merge branch 'canary' into i18n/test-amp
2 parents fc0715d + 2a94ae0 commit 912838d

File tree

12 files changed

+283
-33
lines changed

12 files changed

+283
-33
lines changed

packages/next/build/index.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export type PrerenderManifest = {
9696
version: 2
9797
routes: { [route: string]: SsgRoute }
9898
dynamicRoutes: { [route: string]: DynamicSsgRoute }
99+
notFoundRoutes: string[]
99100
preview: __ApiPreviewProps
100101
}
101102

@@ -713,6 +714,7 @@ export default async function build(
713714

714715
const finalPrerenderRoutes: { [route: string]: SsgRoute } = {}
715716
const tbdPrerenderRoutes: string[] = []
717+
let ssgNotFoundPaths: string[] = []
716718

717719
if (postCompileSpinner) postCompileSpinner.stopAndPersist()
718720

@@ -730,6 +732,7 @@ export default async function build(
730732
const exportConfig: any = {
731733
...config,
732734
initialPageRevalidationMap: {},
735+
ssgNotFoundPaths: [] as string[],
733736
// Default map will be the collection of automatic statically exported
734737
// pages and incremental pages.
735738
// n.b. we cannot handle this above in combinedPages because the dynamic
@@ -821,6 +824,7 @@ export default async function build(
821824
const postBuildSpinner = createSpinner({
822825
prefixText: `${Log.prefixes.info} Finalizing page optimization`,
823826
})
827+
ssgNotFoundPaths = exportConfig.ssgNotFoundPaths
824828

825829
// remove server bundles that were exported
826830
for (const page of staticPages) {
@@ -874,11 +878,12 @@ export default async function build(
874878
}
875879

876880
const { i18n } = config.experimental
881+
const isNotFound = ssgNotFoundPaths.includes(page)
877882

878883
// for SSG files with i18n the non-prerendered variants are
879884
// output with the locale prefixed so don't attempt moving
880885
// without the prefix
881-
if (!i18n || additionalSsgFile) {
886+
if ((!i18n || additionalSsgFile) && !isNotFound) {
882887
await promises.mkdir(path.dirname(dest), { recursive: true })
883888
await promises.rename(orig, dest)
884889
} else if (i18n && !isSsg) {
@@ -891,9 +896,14 @@ export default async function build(
891896
if (additionalSsgFile) return
892897

893898
for (const locale of i18n.locales) {
899+
const curPath = `/${locale}${page === '/' ? '' : page}`
894900
const localeExt = page === '/' ? path.extname(file) : ''
895901
const relativeDestNoPages = relativeDest.substr('pages/'.length)
896902

903+
if (isSsg && ssgNotFoundPaths.includes(curPath)) {
904+
continue
905+
}
906+
897907
const updatedRelativeDest = path.join(
898908
'pages',
899909
locale + localeExt,
@@ -913,9 +923,7 @@ export default async function build(
913923
)
914924

915925
if (!isSsg) {
916-
pagesManifest[
917-
`/${locale}${page === '/' ? '' : page}`
918-
] = updatedRelativeDest
926+
pagesManifest[curPath] = updatedRelativeDest
919927
}
920928
await promises.mkdir(path.dirname(updatedDest), { recursive: true })
921929
await promises.rename(updatedOrig, updatedDest)
@@ -1066,6 +1074,7 @@ export default async function build(
10661074
version: 2,
10671075
routes: finalPrerenderRoutes,
10681076
dynamicRoutes: finalDynamicRoutes,
1077+
notFoundRoutes: ssgNotFoundPaths,
10691078
preview: previewProps,
10701079
}
10711080

@@ -1085,6 +1094,7 @@ export default async function build(
10851094
routes: {},
10861095
dynamicRoutes: {},
10871096
preview: previewProps,
1097+
notFoundRoutes: [],
10881098
}
10891099
await promises.writeFile(
10901100
path.join(distDir, PRERENDER_MANIFEST),

packages/next/export/index.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -473,13 +473,17 @@ export default async function exportApp(
473473
renderError = renderError || !!result.error
474474
if (!!result.error) errorPaths.push(path)
475475

476-
if (
477-
options.buildExport &&
478-
typeof result.fromBuildExportRevalidate !== 'undefined'
479-
) {
480-
configuration.initialPageRevalidationMap[path] =
481-
result.fromBuildExportRevalidate
476+
if (options.buildExport) {
477+
if (typeof result.fromBuildExportRevalidate !== 'undefined') {
478+
configuration.initialPageRevalidationMap[path] =
479+
result.fromBuildExportRevalidate
480+
}
481+
482+
if (result.ssgNotFound === true) {
483+
configuration.ssgNotFoundPaths.push(path)
484+
}
482485
}
486+
483487
if (progress) progress()
484488
})
485489
)

packages/next/export/worker.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ interface ExportPageResults {
5555
ampValidations: AmpValidation[]
5656
fromBuildExportRevalidate?: number
5757
error?: boolean
58+
ssgNotFound?: boolean
5859
}
5960

6061
interface RenderOpts {
@@ -258,11 +259,11 @@ export default async function exportPage({
258259
// @ts-ignore
259260
params
260261
)
261-
curRenderOpts = result.renderOpts || {}
262-
html = result.html
262+
curRenderOpts = (result as any).renderOpts || {}
263+
html = (result as any).html
263264
}
264265

265-
if (!html) {
266+
if (!html && !(curRenderOpts as any).ssgNotFound) {
266267
throw new Error(`Failed to render serverless page`)
267268
}
268269
} else {
@@ -317,6 +318,7 @@ export default async function exportPage({
317318
html = await renderMethod(req, res, page, query, curRenderOpts)
318319
}
319320
}
321+
results.ssgNotFound = (curRenderOpts as any).ssgNotFound
320322

321323
const validateAmp = async (
322324
rawAmpHtml: string,
@@ -340,7 +342,9 @@ export default async function exportPage({
340342
}
341343

342344
if (curRenderOpts.inAmpMode && !curRenderOpts.ampSkipValidation) {
343-
await validateAmp(html, path, curRenderOpts.ampValidatorPath)
345+
if (!results.ssgNotFound) {
346+
await validateAmp(html, path, curRenderOpts.ampValidatorPath)
347+
}
344348
} else if (curRenderOpts.hybridAmp) {
345349
// we need to render the AMP version
346350
let ampHtmlFilename = `${ampPath}${sep}index.html`
@@ -409,6 +413,10 @@ export default async function exportPage({
409413
}
410414
results.fromBuildExportRevalidate = (curRenderOpts as any).revalidate
411415

416+
if (results.ssgNotFound) {
417+
// don't attempt writing to disk if getStaticProps returned not found
418+
return results
419+
}
412420
await promises.writeFile(htmlFilepath, html, 'utf8')
413421
return results
414422
} catch (error) {

packages/next/next-server/lib/router/router.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,8 @@ const manualScrollRestoration =
299299
typeof window !== 'undefined' &&
300300
'scrollRestoration' in window.history
301301

302+
const SSG_DATA_NOT_FOUND_ERROR = 'SSG Data NOT_FOUND'
303+
302304
function fetchRetry(url: string, attempts: number): Promise<any> {
303305
return fetch(url, {
304306
// Cookies are required to be present for Next.js' SSG "Preview Mode".
@@ -318,9 +320,13 @@ function fetchRetry(url: string, attempts: number): Promise<any> {
318320
if (attempts > 1 && res.status >= 500) {
319321
return fetchRetry(url, attempts - 1)
320322
}
323+
if (res.status === 404) {
324+
// TODO: handle reloading in development from fallback returning 200
325+
// to on-demand-entry-handler causing it to reload periodically
326+
throw new Error(SSG_DATA_NOT_FOUND_ERROR)
327+
}
321328
throw new Error(`Failed to load static props`)
322329
}
323-
324330
return res.json()
325331
})
326332
}
@@ -330,7 +336,8 @@ function fetchNextData(dataHref: string, isServerRender: boolean) {
330336
// We should only trigger a server-side transition if this was caused
331337
// on a client-side transition. Otherwise, we'd get into an infinite
332338
// loop.
333-
if (!isServerRender) {
339+
340+
if (!isServerRender || err.message === 'SSG Data NOT_FOUND') {
334341
markLoadingError(err)
335342
}
336343
throw err
@@ -900,6 +907,13 @@ export default class Router implements BaseRouter {
900907
// 3. Internal error while loading the page
901908

902909
// So, doing a hard reload is the proper way to deal with this.
910+
if (process.env.NODE_ENV === 'development') {
911+
// append __next404 query to prevent fallback from being re-served
912+
// on reload in development
913+
if (err.message === SSG_DATA_NOT_FOUND_ERROR && this.isSsr) {
914+
as += `${as.indexOf('?') > -1 ? '&' : '?'}__next404=1`
915+
}
916+
}
903917
window.location.href = as
904918

905919
// Changing the URL doesn't block executing the current code path.

packages/next/next-server/server/incremental-cache.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ function toRoute(pathname: string): string {
1010
}
1111

1212
type IncrementalCacheValue = {
13-
html: string
14-
pageData: any
13+
html?: string
14+
pageData?: any
1515
isStale?: boolean
16+
isNotFound?: boolean
1617
curRevalidate?: number | false
1718
// milliseconds to revalidate after
1819
revalidateAfter: number | false
@@ -55,6 +56,7 @@ export class IncrementalCache {
5556
version: -1 as any, // letting us know this doesn't conform to spec
5657
routes: {},
5758
dynamicRoutes: {},
59+
notFoundRoutes: [],
5860
preview: null as any, // `preview` is special case read in next-dev-server
5961
}
6062
} else {
@@ -67,8 +69,9 @@ export class IncrementalCache {
6769
// default to 50MB limit
6870
max: max || 50 * 1024 * 1024,
6971
length(val) {
72+
if (val.isNotFound) return 25
7073
// rough estimate of size of cache value
71-
return val.html.length + JSON.stringify(val.pageData).length
74+
return val.html!.length + JSON.stringify(val.pageData).length
7275
},
7376
})
7477
}
@@ -112,6 +115,10 @@ export class IncrementalCache {
112115

113116
// let's check the disk for seed data
114117
if (!data) {
118+
if (this.prerenderManifest.notFoundRoutes.includes(pathname)) {
119+
return { isNotFound: true, revalidateAfter: false }
120+
}
121+
115122
try {
116123
const html = await promises.readFile(
117124
this.getSeedPath(pathname, 'html'),
@@ -151,8 +158,9 @@ export class IncrementalCache {
151158
async set(
152159
pathname: string,
153160
data: {
154-
html: string
155-
pageData: any
161+
html?: string
162+
pageData?: any
163+
isNotFound?: boolean
156164
},
157165
revalidateSeconds?: number | false
158166
) {
@@ -178,7 +186,7 @@ export class IncrementalCache {
178186

179187
// TODO: This option needs to cease to exist unless it stops mutating the
180188
// `next build` output's manifest.
181-
if (this.incrementalOptions.flushToDisk) {
189+
if (this.incrementalOptions.flushToDisk && !data.isNotFound) {
182190
try {
183191
const seedPath = this.getSeedPath(pathname, 'html')
184192
await promises.mkdir(path.dirname(seedPath), { recursive: true })

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

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,7 @@ export default class Server {
700700
)
701701

702702
const { query } = parsedDestination
703-
delete parsedDestination.query
703+
delete (parsedDestination as any).query
704704

705705
parsedDestination.search = stringifyQs(query, undefined, undefined, {
706706
encodeURIComponent: (str: string) => str,
@@ -745,7 +745,7 @@ export default class Server {
745745
// external rewrite, proxy it
746746
if (parsedDestination.protocol) {
747747
const { query } = parsedDestination
748-
delete parsedDestination.query
748+
delete (parsedDestination as any).query
749749
parsedDestination.search = stringifyQs(
750750
query,
751751
undefined,
@@ -1116,6 +1116,7 @@ export default class Server {
11161116
...(components.getStaticProps
11171117
? {
11181118
amp: query.amp,
1119+
__next404: query.__next404,
11191120
_nextDataReq: query._nextDataReq,
11201121
__nextLocale: query.__nextLocale,
11211122
}
@@ -1241,12 +1242,27 @@ export default class Server {
12411242
query.amp ? '.amp' : ''
12421243
}`
12431244

1245+
// In development we use a __next404 query to allow signaling we should
1246+
// render the 404 page after attempting to fetch the _next/data for a
1247+
// fallback page since the fallback page will always be available after
1248+
// reload and we don't want to re-serve it and instead want to 404.
1249+
if (this.renderOpts.dev && isSSG && query.__next404) {
1250+
delete query.__next404
1251+
throw new NoFallbackError()
1252+
}
1253+
12441254
// Complete the response with cached data if its present
12451255
const cachedData = ssgCacheKey
12461256
? await this.incrementalCache.get(ssgCacheKey)
12471257
: undefined
12481258

12491259
if (cachedData) {
1260+
if (cachedData.isNotFound) {
1261+
// we don't currently revalidate when notFound is returned
1262+
// so trigger rendering 404 here
1263+
throw new NoFallbackError()
1264+
}
1265+
12501266
const data = isDataReq
12511267
? JSON.stringify(cachedData.pageData)
12521268
: cachedData.html
@@ -1291,10 +1307,12 @@ export default class Server {
12911307
html: string | null
12921308
pageData: any
12931309
sprRevalidate: number | false
1310+
isNotFound?: boolean
12941311
}> => {
12951312
let pageData: any
12961313
let html: string | null
12971314
let sprRevalidate: number | false
1315+
let isNotFound: boolean | undefined
12981316

12991317
let renderResult
13001318
// handle serverless
@@ -1314,6 +1332,7 @@ export default class Server {
13141332
html = renderResult.html
13151333
pageData = renderResult.renderOpts.pageData
13161334
sprRevalidate = renderResult.renderOpts.revalidate
1335+
isNotFound = renderResult.renderOpts.ssgNotFound
13171336
} else {
13181337
const origQuery = parseUrl(req.url || '', true).query
13191338
const resolvedUrl = formatUrl({
@@ -1355,9 +1374,10 @@ export default class Server {
13551374
// TODO: change this to a different passing mechanism
13561375
pageData = (renderOpts as any).pageData
13571376
sprRevalidate = (renderOpts as any).revalidate
1377+
isNotFound = (renderOpts as any).ssgNotFound
13581378
}
13591379

1360-
return { html, pageData, sprRevalidate }
1380+
return { html, pageData, sprRevalidate, isNotFound }
13611381
}
13621382
)
13631383

@@ -1439,10 +1459,15 @@ export default class Server {
14391459

14401460
const {
14411461
isOrigin,
1442-
value: { html, pageData, sprRevalidate },
1462+
value: { html, pageData, sprRevalidate, isNotFound },
14431463
} = await doRender()
14441464
let resHtml = html
1445-
if (!isResSent(res) && (isSSG || isDataReq || isServerProps)) {
1465+
1466+
if (
1467+
!isResSent(res) &&
1468+
!isNotFound &&
1469+
(isSSG || isDataReq || isServerProps)
1470+
) {
14461471
sendPayload(
14471472
req,
14481473
res,
@@ -1467,11 +1492,14 @@ export default class Server {
14671492
if (isOrigin && ssgCacheKey) {
14681493
await this.incrementalCache.set(
14691494
ssgCacheKey,
1470-
{ html: html!, pageData },
1495+
{ html: html!, pageData, isNotFound },
14711496
sprRevalidate
14721497
)
14731498
}
14741499

1500+
if (isNotFound) {
1501+
throw new NoFallbackError()
1502+
}
14751503
return resHtml
14761504
}
14771505

0 commit comments

Comments
 (0)