Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix app static generation cases #41172

Merged
merged 11 commits into from Oct 6, 2022
31 changes: 19 additions & 12 deletions packages/next/client/components/reducer.ts
Expand Up @@ -237,7 +237,6 @@ function fillCacheWithPrefetchedSubTreeData(

const existingChildCacheNode = existingChildSegmentMap.get(segmentForCache)

// In case of last segment start the fetch at this level and don't copy further down.
if (isLastEntry) {
if (!existingChildCacheNode) {
existingChildSegmentMap.set(segmentForCache, {
Expand Down Expand Up @@ -772,8 +771,6 @@ function clientReducer(
href
)

// Fill in the cache with blank that holds the `data` field.
// TODO-APP: segments.slice(1) strips '', we can get rid of '' altogether.
// Copy subTreeData for the root node of the cache.
cache.subTreeData = state.cache.subTreeData

Expand All @@ -782,6 +779,7 @@ function clientReducer(
const res = fillCacheWithDataProperty(
cache,
state.cache,
// TODO-APP: segments.slice(1) strips '', we can get rid of '' altogether.
segments.slice(1),
() => fetchServerResponse(url, optimisticTree)
)
Expand Down Expand Up @@ -839,7 +837,7 @@ function clientReducer(
const flightDataPath = flightData[0]

// The one before last item is the router state tree patch
const [treePatch] = flightDataPath.slice(-2)
const [treePatch, subTreeData] = flightDataPath.slice(-2)

// Path without the last segment, router state, and the subTreeData
const flightSegmentPath = flightDataPath.slice(0, -3)
Expand All @@ -865,10 +863,14 @@ function clientReducer(
mutable.previousTree = state.tree
mutable.patchedTree = newTree

// Copy subTreeData for the root node of the cache.
cache.subTreeData = state.cache.subTreeData
// Create a copy of the existing cache with the subTreeData applied.
fillCacheWithNewSubTreeData(cache, state.cache, flightDataPath)
if (flightDataPath.length === 2) {
cache.subTreeData = subTreeData
} else {
// Copy subTreeData for the root node of the cache.
cache.subTreeData = state.cache.subTreeData
// Create a copy of the existing cache with the subTreeData applied.
fillCacheWithNewSubTreeData(cache, state.cache, flightDataPath)
}

return {
// Set href.
Expand Down Expand Up @@ -939,7 +941,7 @@ function clientReducer(

// Slices off the last segment (which is at -3) as it doesn't exist in the tree yet
const treePath = flightDataPath.slice(0, -3)
const [treePatch] = flightDataPath.slice(-2)
const [treePatch, subTreeData] = flightDataPath.slice(-2)

const newTree = applyRouterStatePatchToTree(
// TODO-APP: remove ''
Expand All @@ -962,9 +964,14 @@ function clientReducer(

mutable.patchedTree = newTree

// Copy subTreeData for the root node of the cache.
cache.subTreeData = state.cache.subTreeData
fillCacheWithNewSubTreeData(cache, state.cache, flightDataPath)
// Root refresh
if (flightDataPath.length === 2) {
cache.subTreeData = subTreeData
} else {
// Copy subTreeData for the root node of the cache.
cache.subTreeData = state.cache.subTreeData
fillCacheWithNewSubTreeData(cache, state.cache, flightDataPath)
}

return {
// Keep href as it was set during navigate / restore
Expand Down
5 changes: 3 additions & 2 deletions packages/next/export/worker.ts
Expand Up @@ -96,6 +96,7 @@ interface RenderOpts {
defaultLocale?: string
domainLocales?: DomainLocale[]
trailingSlash?: boolean
supportsDynamicHTML?: boolean
}

type ComponentModule = ComponentType<{}> & {
Expand Down Expand Up @@ -389,6 +390,7 @@ export default async function exportPage({
? requireFontManifest(distDir, serverless)
: null,
locale: locale as string,
supportsDynamicHTML: false,
}

// during build we attempt rendering app dir paths
Expand All @@ -406,8 +408,7 @@ export default async function exportPage({
res as any,
page,
query,
curRenderOpts as any,
true
curRenderOpts as any
)
const html = result?.toUnchunkedString()
const flightData = (curRenderOpts as any).pageData
Expand Down
93 changes: 53 additions & 40 deletions packages/next/server/app-render.tsx
Expand Up @@ -33,6 +33,7 @@ import { REDIRECT_ERROR_CODE } from '../client/components/redirect'
import { NextCookies } from './web/spec-extension/cookies'
import { DYNAMIC_ERROR_CODE } from '../client/components/hooks-server-context'
import { NOT_FOUND_ERROR_CODE } from '../client/components/not-found'
import { Writable } from 'stream'

const INTERNAL_HEADERS_INSTANCE = Symbol('internal for headers readonly')

Expand Down Expand Up @@ -658,9 +659,22 @@ export async function renderToHTMLOrFlight(
res: ServerResponse,
pathname: string,
query: NextParsedUrlQuery,
renderOpts: RenderOpts,
isStaticGeneration: boolean = false
renderOpts: RenderOpts
): Promise<RenderResult | null> {
/**
* Rules of Static & Dynamic HTML:
*
* 1.) We must generate static HTML unless the caller explicitly opts
* in to dynamic HTML support.
*
* 2.) If dynamic HTML support is requested, we must honor that request
* or throw an error. It is the sole responsibility of the caller to
* ensure they aren't e.g. requesting dynamic HTML for an AMP page.
*
* These rules help ensure that other existing features like request caching,
* coalescing, and ISR continue working as intended.
*/
const isStaticGeneration = renderOpts.supportsDynamicHTML !== true
const isFlight = req.headers.__rsc__ !== undefined

const capturedErrors: Error[] = []
Expand All @@ -683,7 +697,6 @@ export async function renderToHTMLOrFlight(
subresourceIntegrityManifest,
serverComponentManifest,
serverCSSManifest = {},
supportsDynamicHTML,
ComponentMod,
dev,
} = renderOpts
Expand Down Expand Up @@ -721,10 +734,10 @@ export async function renderToHTMLOrFlight(
/**
* Router state provided from the client-side router. Used to handle rendering from the common layout down.
*/
const providedFlightRouterState: FlightRouterState = isFlight
let providedFlightRouterState: FlightRouterState = isFlight
? req.headers.__next_router_state_tree__
? JSON.parse(req.headers.__next_router_state_tree__ as string)
: {}
: undefined
: undefined

/**
Expand Down Expand Up @@ -1104,23 +1117,22 @@ export async function renderToHTMLOrFlight(
}
}

/**
* Rules of Static & Dynamic HTML:
*
* 1.) We must generate static HTML unless the caller explicitly opts
* in to dynamic HTML support.
*
* 2.) If dynamic HTML support is requested, we must honor that request
* or throw an error. It is the sole responsibility of the caller to
* ensure they aren't e.g. requesting dynamic HTML for an AMP page.
*
* These rules help ensure that other existing features like request caching,
* coalescing, and ISR continue working as intended.
*/
const generateStaticHTML = supportsDynamicHTML !== true
const streamToBufferedResult = async (
renderResult: RenderResult
): Promise<string> => {
const renderChunks: Buffer[] = []
const writable = new Writable({
write(chunk, _encoding, callback) {
renderChunks.push(chunk)
callback()
},
})
await renderResult.pipe(writable)
return Buffer.concat(renderChunks).toString()
}

// Handle Flight render request. This is only used when client-side navigating. E.g. when you `router.push('/dashboard')` or `router.reload()`.
if (isFlight) {
const generateFlight = async (): Promise<RenderResult> => {
// TODO-APP: throw on invalid flightRouterState
/**
* Use router state to decide at what common layout to render the page.
Expand Down Expand Up @@ -1252,15 +1264,13 @@ export async function renderToHTMLOrFlight(
}
).pipeThrough(createBufferedTransformStream())

if (generateStaticHTML) {
let staticHtml = Buffer.from(
(await readable.getReader().read()).value || ''
).toString()
return new FlightRenderResult(staticHtml)
}
return new FlightRenderResult(readable)
}

if (isFlight && !isStaticGeneration) {
return generateFlight()
}

// Below this line is handling for rendering to HTML.

// Create full component tree from root to leaf.
Expand Down Expand Up @@ -1394,7 +1404,7 @@ export async function renderToHTMLOrFlight(

return await continueFromInitialStream(renderStream, {
dataStream: serverComponentsInlinedTransformStream?.readable,
generateStaticHTML: generateStaticHTML,
generateStaticHTML: isStaticGeneration,
getServerInsertedHTML,
serverInsertedHTMLToHead: true,
polyfills,
Expand Down Expand Up @@ -1426,39 +1436,42 @@ export async function renderToHTMLOrFlight(

return await continueFromInitialStream(renderStream, {
dataStream: serverComponentsInlinedTransformStream?.readable,
generateStaticHTML: generateStaticHTML,
generateStaticHTML: isStaticGeneration,
getServerInsertedHTML,
serverInsertedHTMLToHead: true,
polyfills,
dev,
})
}
}
const renderResult = new RenderResult(await bodyResult())

if (generateStaticHTML) {
const readable = await bodyResult()
let staticHtml = Buffer.from(
(await readable.getReader().read()).value || ''
).toString()

if (isStaticGeneration) {
const htmlResult = await streamToBufferedResult(renderResult)
// if we encountered any unexpected errors during build
// we fail the prerendering phase and the build
if (capturedErrors.length > 0) {
throw capturedErrors[0]
}
// const before = Buffer.concat(
// serverComponentsRenderOpts.rscChunks
// ).toString()

// TODO-APP: derive this from same pass to prevent additional
// render during static generation
const filteredFlightData = await streamToBufferedResult(
await generateFlight()
)

;(renderOpts as any).pageData = Buffer.concat(
serverComponentsRenderOpts.rscChunks
).toString()
;(renderOpts as any).pageData = filteredFlightData
;(renderOpts as any).revalidate =
typeof staticGenerationStore?.revalidate === 'undefined'
? defaultRevalidate
: staticGenerationStore?.revalidate

return new RenderResult(staticHtml)
return new RenderResult(htmlResult)
}

return new RenderResult(await bodyResult())
return renderResult
}

const initialStaticGenerationStore = {
Expand Down