Skip to content

Commit

Permalink
Fix app static generation cases (vercel#41172)
Browse files Browse the repository at this point in the history
Co-authored-by: Tim Neutkens <tim@timneutkens.nl>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored and Kikobeats committed Oct 24, 2022
1 parent f65268a commit 593009a
Show file tree
Hide file tree
Showing 19 changed files with 294 additions and 82 deletions.
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 @@ -34,6 +34,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 @@ -694,9 +695,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 @@ -719,7 +733,6 @@ export async function renderToHTMLOrFlight(
subresourceIntegrityManifest,
serverComponentManifest,
serverCSSManifest = {},
supportsDynamicHTML,
ComponentMod,
dev,
fontLoaderManifest,
Expand Down Expand Up @@ -758,10 +771,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 @@ -1160,23 +1173,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 @@ -1308,15 +1320,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 @@ -1450,7 +1460,7 @@ export async function renderToHTMLOrFlight(

return await continueFromInitialStream(renderStream, {
dataStream: serverComponentsInlinedTransformStream?.readable,
generateStaticHTML: generateStaticHTML,
generateStaticHTML: isStaticGeneration,
getServerInsertedHTML,
serverInsertedHTMLToHead: true,
polyfills,
Expand Down Expand Up @@ -1482,39 +1492,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

0 comments on commit 593009a

Please sign in to comment.