From e2a14bf3d40795b1ecc182cdd0c055cbcf66e671 Mon Sep 17 00:00:00 2001 From: Ty Hopp Date: Fri, 4 Nov 2022 19:10:09 +0800 Subject: [PATCH] feat(gatsby): Slices <> partial hydration interop (#36960) * Add use client directive to slice placeholder * DRY loader * Remove either-or condition * Adjust conditional logic * Add slice providers to tree * Do not remove slice dependency blocks from bundle Co-authored-by: Michal Piechowiak * skip indirection when importing static query context * collect static queries from slices when rendering rsc Co-authored-by: Michal Piechowiak --- packages/gatsby/cache-dir/loader.js | 388 +++++++----------- packages/gatsby/cache-dir/production-app.js | 47 +-- packages/gatsby/cache-dir/slice.js | 6 +- .../gatsby/src/bootstrap/load-config/index.ts | 8 +- .../webpack/plugins/partial-hydration.ts | 12 + .../src/utils/worker/child/render-html.ts | 19 +- 6 files changed, 208 insertions(+), 272 deletions(-) diff --git a/packages/gatsby/cache-dir/loader.js b/packages/gatsby/cache-dir/loader.js index 976431cab30ad..eba4819db3b57 100644 --- a/packages/gatsby/cache-dir/loader.js +++ b/packages/gatsby/cache-dir/loader.js @@ -361,74 +361,173 @@ export class BaseLoader { return this.inFlightDb.get(pagePath) } - let inFlightPromise + const loadDataPromises = [ + this.loadAppData(), + this.loadPageDataJson(pagePath), + ] + if (global.hasPartialHydration) { - inFlightPromise = Promise.all([ - this.loadAppData(), - this.loadPageDataJson(pagePath), - this.loadPartialHydrationJson(pagePath), - ]).then(([appData, { payload: pageData }, result]) => { - if (result.status === PageResourceStatus.Error) { - return { - status: PageResourceStatus.Error, + loadDataPromises.push(this.loadPartialHydrationJson(pagePath)) + } + + const inFlightPromise = Promise.all(loadDataPromises).then(allData => { + const [appDataResponse, pageDataResponse, rscDataResponse] = allData + + if ( + pageDataResponse.status === PageResourceStatus.Error || + rscDataResponse?.status === PageResourceStatus.Error + ) { + return { + status: PageResourceStatus.Error, + } + } + + let pageData = pageDataResponse.payload + + const { + componentChunkName, + staticQueryHashes: pageStaticQueryHashes = [], + slicesMap = {}, + } = pageData + + const finalResult = {} + + const dedupedSliceNames = Array.from(new Set(Object.values(slicesMap))) + + const loadSlice = slice => { + if (this.slicesDb.has(slice.name)) { + return this.slicesDb.get(slice.name) + } else if (this.sliceInflightDb.has(slice.name)) { + return this.sliceInflightDb.get(slice.name) + } + + const inFlight = this.loadComponent(slice.componentChunkName).then( + component => { + return { + component: preferDefault(component), + sliceContext: slice.result.sliceContext, + data: slice.result.data, + } + } + ) + + this.sliceInflightDb.set(slice.name, inFlight) + inFlight.then(results => { + this.slicesDb.set(slice.name, results) + this.sliceInflightDb.delete(slice.name) + }) + + return inFlight + } + + return Promise.all( + dedupedSliceNames.map(sliceName => this.loadSliceDataJson(sliceName)) + ).then(slicesData => { + const slices = [] + const dedupedStaticQueryHashes = [...pageStaticQueryHashes] + + for (const { jsonPayload, sliceName } of Object.values(slicesData)) { + slices.push({ name: sliceName, ...jsonPayload }) + for (const staticQueryHash of jsonPayload.staticQueryHashes) { + if (!dedupedStaticQueryHashes.includes(staticQueryHash)) { + dedupedStaticQueryHashes.push(staticQueryHash) + } } } - const finalResult = {} + const loadChunkPromises = [ + Promise.all(slices.map(loadSlice)), + this.loadComponent(componentChunkName, `head`), + ] + + if (!global.hasPartialHydration) { + loadChunkPromises.push(this.loadComponent(componentChunkName)) + } // In develop we have separate chunks for template and Head components // to enable HMR (fast refresh requires single exports). // In production we have shared chunk with both exports. Double loadComponent here // will be deduped by webpack runtime resulting in single request and single module // being loaded for both `component` and `head`. - const componentChunkPromise = this.loadComponent( - pageData.componentChunkName, - `head` - ).then(head => { - finalResult.createdAt = new Date() - finalResult.status = PageResourceStatus.Success - if (result.notFound === true) { - finalResult.notFound = true - } - pageData = Object.assign(pageData, { - webpackCompilationHash: appData - ? appData.webpackCompilationHash - : ``, - }) + // get list of components to get + const componentChunkPromises = Promise.all(loadChunkPromises).then( + components => { + const [sliceComponents, headComponent, pageComponent] = components - const pageResources = toPageResources(pageData, null, head) - - if (result.payload && typeof result.payload === `string`) { - pageResources.partialHydration = result.payload - - const readableStream = new ReadableStream({ - start(controller) { - const te = new TextEncoder() - controller.enqueue(te.encode(result.payload)) - }, - pull(controller) { - // close on next read when queue is empty - controller.close() - }, - cancel() {}, - }) + finalResult.createdAt = new Date() + + for (const sliceComponent of sliceComponents) { + if (!sliceComponent || sliceComponent instanceof Error) { + finalResult.status = PageResourceStatus.Error + finalResult.error = sliceComponent + } + } - return waitForResponse( - createFromReadableStream(readableStream) - ).then(result => { - pageResources.partialHydration = result + if ( + !global.hasPartialHydration && + (!pageComponent || pageComponent instanceof Error) + ) { + finalResult.status = PageResourceStatus.Error + finalResult.error = pageComponent + } - return pageResources - }) - } + let pageResources - // undefined if final result is an error - return pageResources - }) + if (finalResult.status !== PageResourceStatus.Error) { + finalResult.status = PageResourceStatus.Success + if ( + pageDataResponse.notFound === true || + rscDataResponse?.notFound === true + ) { + finalResult.notFound = true + } + pageData = Object.assign(pageData, { + webpackCompilationHash: appDataResponse + ? appDataResponse.webpackCompilationHash + : ``, + }) + + if (typeof rscDataResponse?.payload === `string`) { + pageResources = toPageResources(pageData, null, headComponent) + + pageResources.partialHydration = rscDataResponse.payload + + const readableStream = new ReadableStream({ + start(controller) { + const te = new TextEncoder() + controller.enqueue(te.encode(rscDataResponse.payload)) + }, + pull(controller) { + // close on next read when queue is empty + controller.close() + }, + cancel() {}, + }) + + return waitForResponse( + createFromReadableStream(readableStream) + ).then(result => { + pageResources.partialHydration = result + + return pageResources + }) + } else { + pageResources = toPageResources( + pageData, + pageComponent, + headComponent + ) + } + } - // Necessary for head component + // undefined if final result is an error + return pageResources + } + ) + + // get list of static queries to get const staticQueryBatchPromise = Promise.all( - (pageData.staticQueryHashes || []).map(staticQueryHash => { + dedupedStaticQueryHashes.map(staticQueryHash => { // Check for cache in case this static query result has already been loaded if (this.staticQueryDb[staticQueryHash]) { const jsonPayload = this.staticQueryDb[staticQueryHash] @@ -460,14 +559,11 @@ export class BaseLoader { }) return ( - Promise.all([componentChunkPromise, staticQueryBatchPromise]) + Promise.all([componentChunkPromises, staticQueryBatchPromise]) .then(([pageResources, staticQueryResults]) => { let payload if (pageResources) { - payload = { - ...pageResources, - staticQueryResults: staticQueryResults, - } + payload = { ...pageResources, staticQueryResults } finalResult.payload = payload emitter.emit(`onPostLoadPageResources`, { page: payload, @@ -495,183 +591,7 @@ export class BaseLoader { }) ) }) - } else { - inFlightPromise = Promise.all([ - this.loadAppData(), - this.loadPageDataJson(pagePath), - ]).then(allData => { - const result = allData[1] - if (result.status === PageResourceStatus.Error) { - return { - status: PageResourceStatus.Error, - } - } - - let pageData = result.payload - const { - componentChunkName, - staticQueryHashes: pageStaticQueryHashes = [], - slicesMap = {}, - } = pageData - - const finalResult = {} - - const dedupedSliceNames = Array.from(new Set(Object.values(slicesMap))) - - const loadSlice = slice => { - if (this.slicesDb.has(slice.name)) { - return this.slicesDb.get(slice.name) - } else if (this.sliceInflightDb.has(slice.name)) { - return this.sliceInflightDb.get(slice.name) - } - - const inFlight = this.loadComponent(slice.componentChunkName).then( - component => { - return { - component: preferDefault(component), - sliceContext: slice.result.sliceContext, - data: slice.result.data, - } - } - ) - - this.sliceInflightDb.set(slice.name, inFlight) - inFlight.then(results => { - this.slicesDb.set(slice.name, results) - this.sliceInflightDb.delete(slice.name) - }) - - return inFlight - } - - return Promise.all( - dedupedSliceNames.map(sliceName => this.loadSliceDataJson(sliceName)) - ).then(slicesData => { - const slices = [] - const dedupedStaticQueryHashes = [...pageStaticQueryHashes] - for (const { jsonPayload, sliceName } of Object.values(slicesData)) { - slices.push({ name: sliceName, ...jsonPayload }) - for (const staticQueryHash of jsonPayload.staticQueryHashes) { - if (!dedupedStaticQueryHashes.includes(staticQueryHash)) { - dedupedStaticQueryHashes.push(staticQueryHash) - } - } - } - - // In develop we have separate chunks for template and Head components - // to enable HMR (fast refresh requires single exports). - // In production we have shared chunk with both exports. Double loadComponent here - // will be deduped by webpack runtime resulting in single request and single module - // being loaded for both `component` and `head`. - // get list of components to get - const componentChunkPromises = Promise.all([ - this.loadComponent(componentChunkName), - this.loadComponent(componentChunkName, `head`), - Promise.all(slices.map(loadSlice)), - ]).then(components => { - const [rootComponent, headComponent, sliceComponents] = components - finalResult.createdAt = new Date() - for (const sliceComponent of sliceComponents) { - if (!sliceComponent || sliceComponent instanceof Error) { - finalResult.status = PageResourceStatus.Error - finalResult.error = sliceComponent - } - } - - if (!rootComponent || rootComponent instanceof Error) { - finalResult.status = PageResourceStatus.Error - finalResult.error = rootComponent - } - - let pageResources - if (finalResult.status !== PageResourceStatus.Error) { - finalResult.status = PageResourceStatus.Success - if (result.notFound === true) { - finalResult.notFound = true - } - pageData = Object.assign(pageData, { - webpackCompilationHash: allData[0] - ? allData[0].webpackCompilationHash - : ``, - }) - pageResources = toPageResources( - pageData, - rootComponent, - headComponent - ) - } - // undefined if final result is an error - return pageResources - }) - - // get list of static queries to get - const staticQueryBatchPromise = Promise.all( - dedupedStaticQueryHashes.map(staticQueryHash => { - // Check for cache in case this static query result has already been loaded - if (this.staticQueryDb[staticQueryHash]) { - const jsonPayload = this.staticQueryDb[staticQueryHash] - return { staticQueryHash, jsonPayload } - } - - return this.memoizedGet( - `${__PATH_PREFIX__}/page-data/sq/d/${staticQueryHash}.json` - ) - .then(req => { - const jsonPayload = JSON.parse(req.responseText) - return { staticQueryHash, jsonPayload } - }) - .catch(() => { - throw new Error( - `We couldn't load "${__PATH_PREFIX__}/page-data/sq/d/${staticQueryHash}.json"` - ) - }) - }) - ).then(staticQueryResults => { - const staticQueryResultsMap = {} - - staticQueryResults.forEach(({ staticQueryHash, jsonPayload }) => { - staticQueryResultsMap[staticQueryHash] = jsonPayload - this.staticQueryDb[staticQueryHash] = jsonPayload - }) - - return staticQueryResultsMap - }) - - return ( - Promise.all([componentChunkPromises, staticQueryBatchPromise]) - .then(([pageResources, staticQueryResults]) => { - let payload - if (pageResources) { - payload = { ...pageResources, staticQueryResults } - finalResult.payload = payload - emitter.emit(`onPostLoadPageResources`, { - page: payload, - pageResources: payload, - }) - } - - this.pageDb.set(pagePath, finalResult) - - if (finalResult.error) { - return { - error: finalResult.error, - status: finalResult.status, - } - } - - return payload - }) - // when static-query fail to load we throw a better error - .catch(err => { - return { - error: err, - status: PageResourceStatus.Error, - } - }) - ) - }) - }) - } + }) inFlightPromise .then(() => { diff --git a/packages/gatsby/cache-dir/production-app.js b/packages/gatsby/cache-dir/production-app.js index eb047fbc4f0fd..a191a2184e73e 100644 --- a/packages/gatsby/cache-dir/production-app.js +++ b/packages/gatsby/cache-dir/production-app.js @@ -2,7 +2,7 @@ import { apiRunner, apiRunnerAsync } from "./api-runner-browser" import React from "react" import { Router, navigate, Location, BaseContext } from "@gatsbyjs/reach-router" import { ScrollContext } from "gatsby-react-router-scroll" -import { StaticQueryContext } from "gatsby" +import { StaticQueryContext } from "./static-query" import { SlicesMapContext, SlicesContext, @@ -85,33 +85,26 @@ apiRunnerAsync(`onClientEntry`).then(() => { {({ location }) => ( {({ pageResources, location }) => { - if (pageResources.partialHydration) { - return ( - - {children} - - ) - } else { - const staticQueryResults = getStaticQueryResults() - const sliceResults = getSliceResults() - return ( - - - - + + + + - - {children} - - - - - - ) - } + {children} + + + + + + ) }} )} diff --git a/packages/gatsby/cache-dir/slice.js b/packages/gatsby/cache-dir/slice.js index 952fc2f9c2ff5..5614cade4503d 100644 --- a/packages/gatsby/cache-dir/slice.js +++ b/packages/gatsby/cache-dir/slice.js @@ -1,3 +1,5 @@ +"use client" + import React, { useContext } from "react" import { ServerSlice } from "./slice/server-slice" import { InlineSlice } from "./slice/inline-slice" @@ -55,9 +57,7 @@ export function Slice(props) { ) } } else { - throw new Error( - `Slices are disabled, likely due to PARTIAL_HYDRATION flag being set.` - ) + throw new Error(`Slices are disabled.`) } } diff --git a/packages/gatsby/src/bootstrap/load-config/index.ts b/packages/gatsby/src/bootstrap/load-config/index.ts index fec6ffa5c0bbb..8d4e98ef2a0f4 100644 --- a/packages/gatsby/src/bootstrap/load-config/index.ts +++ b/packages/gatsby/src/bootstrap/load-config/index.ts @@ -63,13 +63,7 @@ export async function loadConfig({ reporter.info(message) } - if (process.env.GATSBY_PARTIAL_HYDRATION) { - delete process.env.GATSBY_SLICES - - reporter.warn(`SLICES is inactive when PARTIAL_HYDRATION is enabled.`) - } else { - process.env.GATSBY_SLICES = `true` - } + process.env.GATSBY_SLICES = `true` // track usage of feature enabledConfigFlags.forEach(flag => { diff --git a/packages/gatsby/src/utils/webpack/plugins/partial-hydration.ts b/packages/gatsby/src/utils/webpack/plugins/partial-hydration.ts index 1924c99fcac9e..5da3cbe7a2218 100644 --- a/packages/gatsby/src/utils/webpack/plugins/partial-hydration.ts +++ b/packages/gatsby/src/utils/webpack/plugins/partial-hydration.ts @@ -7,6 +7,7 @@ import webpack, { javascript, Compilation, Compiler, + AsyncDependenciesBlock, } from "webpack" import type Reporter from "gatsby-cli/lib/reporter" @@ -247,6 +248,17 @@ export class PartialHydrationPlugin { for (const connection of incomingConnections) { if (connection.dependency) { + const dependencyBlock = compilation.moduleGraph.getParentBlock( + connection.dependency + ) + + if ( + dependencyBlock instanceof AsyncDependenciesBlock && + dependencyBlock?.chunkName?.startsWith(`slice---`) + ) { + continue + } + if (connection.originModule instanceof NormalModule) { if ( connection.originModule.resource.includes(`async-requires`) diff --git a/packages/gatsby/src/utils/worker/child/render-html.ts b/packages/gatsby/src/utils/worker/child/render-html.ts index 2b9c9abb270db..333ca11462a1b 100644 --- a/packages/gatsby/src/utils/worker/child/render-html.ts +++ b/packages/gatsby/src/utils/worker/child/render-html.ts @@ -351,8 +351,25 @@ export async function renderPartialHydrationProd({ for (const pagePath of paths) { const pageData = await readPageData(publicDir, pagePath) + + // we collect static query hashes from page template and also all used slices on the page + const staticQueryHashes = new Set(pageData.staticQueryHashes) + if (pageData.slicesMap) { + for (const sliceName of Object.values(pageData.slicesMap)) { + const sliceDataPath = path.join( + publicDir, + `slice-data`, + `${sliceName}.json` + ) + const sliceData = await fs.readJSON(sliceDataPath) + for (const staticQueryHash of sliceData.staticQueryHashes) { + staticQueryHashes.add(staticQueryHash) + } + } + } + const { staticQueryContext } = await getStaticQueryContext( - pageData.staticQueryHashes + Array.from(staticQueryHashes) ) const pageRenderer = path.join(