Turbopack: inject chunk group list file into the page for server component CSS (#57376)

### What?

incjects the script tag for CSS reloading

### Why?

### How?


Closes WEB-1851

---------

Co-authored-by: Will Binns-Smith <wbinnssmith@gmail.com>
This commit is contained in:
Tobias Koppers 2023-10-24 20:06:46 -07:00 committed by GitHub
parent 59ebfbea9e
commit 18fe1eb4e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 138 additions and 38 deletions

View file

@ -74,6 +74,11 @@ impl ClientReferenceManifest {
.entry(server_component_name.clone_value())
.or_default();
let entry_js_files = entry_manifest
.entry_js_files
.entry(server_component_name.clone_value())
.or_default();
match app_client_reference_ty {
ClientReferenceType::CssClientReference(_) => {
entry_css_files.extend(
@ -88,9 +93,22 @@ impl ClientReferenceManifest {
})
.map(ToString::to_string),
);
entry_js_files.extend(
client_chunks_paths
.iter()
.filter_map(|chunk_path| {
if chunk_path.extension_ref() != Some("css") {
client_relative_path.get_path_to(chunk_path)
} else {
None
}
})
.map(ToString::to_string),
);
}
ClientReferenceType::EcmascriptClientReference(_) => {
// TODO should this be removed? does it make sense?
entry_css_files.extend(
client_chunks_paths
.iter()

View file

@ -206,6 +206,9 @@ pub struct ClientReferenceManifest {
/// Mapping of server component path to required CSS client chunks.
#[serde(rename = "entryCSSFiles")]
pub entry_css_files: HashMap<String, Vec<String>>,
/// Mapping of server component path to required JS client chunks.
#[serde(rename = "entryJSFiles")]
pub entry_js_files: HashMap<String, Vec<String>>,
}
#[derive(Serialize, Default, Debug)]

View file

@ -76,6 +76,9 @@ export type ClientReferenceManifest = {
entryCSSFiles: {
[entry: string]: string[]
}
entryJSFiles?: {
[entry: string]: string[]
}
}
function getAppPathRequiredChunks(

View file

@ -32,6 +32,7 @@ export interface ErrorBoundaryProps {
children?: React.ReactNode
errorComponent: ErrorComponent
errorStyles?: React.ReactNode | undefined
errorScripts?: React.ReactNode | undefined
}
interface ErrorBoundaryHandlerProps extends ErrorBoundaryProps {
@ -106,6 +107,7 @@ export class ErrorBoundaryHandler extends React.Component<
<>
<HandleISRError error={this.state.error} />
{this.props.errorStyles}
{this.props.errorScripts}
<this.props.errorComponent
error={this.state.error}
reset={this.reset}
@ -158,6 +160,7 @@ export default GlobalError
export function ErrorBoundary({
errorComponent,
errorStyles,
errorScripts,
children,
}: ErrorBoundaryProps & { children: React.ReactNode }): JSX.Element {
const pathname = usePathname()
@ -167,6 +170,7 @@ export function ErrorBoundary({
pathname={pathname}
errorComponent={errorComponent}
errorStyles={errorStyles}
errorScripts={errorScripts}
>
{children}
</ErrorBoundaryHandler>

View file

@ -466,11 +466,13 @@ function LoadingBoundary({
children,
loading,
loadingStyles,
loadingScripts,
hasLoading,
}: {
children: React.ReactNode
loading?: React.ReactNode
loadingStyles?: React.ReactNode
loadingScripts?: React.ReactNode
hasLoading: boolean
}): JSX.Element {
if (hasLoading) {
@ -479,6 +481,7 @@ function LoadingBoundary({
fallback={
<>
{loadingStyles}
{loadingScripts}
{loading}
</>
}
@ -501,9 +504,12 @@ export default function OuterLayoutRouter({
childProp,
error,
errorStyles,
errorScripts,
templateStyles,
templateScripts,
loading,
loadingStyles,
loadingScripts,
hasLoading,
template,
notFound,
@ -515,10 +521,13 @@ export default function OuterLayoutRouter({
childProp: ChildProp
error: ErrorComponent
errorStyles: React.ReactNode | undefined
errorScripts: React.ReactNode | undefined
templateStyles: React.ReactNode | undefined
templateScripts: React.ReactNode | undefined
template: React.ReactNode
loading: React.ReactNode | undefined
loadingStyles: React.ReactNode | undefined
loadingScripts: React.ReactNode | undefined
hasLoading: boolean
notFound: React.ReactNode | undefined
notFoundStyles: React.ReactNode | undefined
@ -580,11 +589,16 @@ export default function OuterLayoutRouter({
key={createRouterCacheKey(preservedSegment, true)}
value={
<ScrollAndFocusHandler segmentPath={segmentPath}>
<ErrorBoundary errorComponent={error} errorStyles={errorStyles}>
<ErrorBoundary
errorComponent={error}
errorStyles={errorStyles}
errorScripts={errorScripts}
>
<LoadingBoundary
hasLoading={hasLoading}
loading={loading}
loadingStyles={loadingStyles}
loadingScripts={loadingScripts}
>
<NotFoundBoundary
notFound={notFound}
@ -611,6 +625,7 @@ export default function OuterLayoutRouter({
}
>
{templateStyles}
{templateScripts}
{template}
</TemplateContext.Provider>
)

View file

@ -273,6 +273,7 @@ async function generateFlight(
<MetadataTree key={requestId} />
),
injectedCSS: new Set(),
injectedJS: new Set(),
injectedFontPreloadTags: new Set(),
rootLayoutIncluded: false,
asNotFound: ctx.isNotFoundPath || options?.asNotFound,
@ -318,6 +319,7 @@ function createServerComponentsRenderer(
preinitScripts()
// Create full component tree from root to leaf.
const injectedCSS = new Set<string>()
const injectedJS = new Set<string>()
const injectedFontPreloadTags = new Set<string>()
const {
getDynamicParamFromSegment,
@ -349,6 +351,7 @@ function createServerComponentsRenderer(
parentParams: {},
firstItem: true,
injectedCSS,
injectedJS,
injectedFontPreloadTags,
rootLayoutIncluded: false,
asNotFound: props.asNotFound,

View file

@ -1,24 +1,27 @@
import React from 'react'
import { interopDefault } from './interop-default'
import { getCssInlinedLinkTags } from './get-css-inlined-link-tags'
import { getLinkAndScriptTags } from './get-css-inlined-link-tags'
import type { AppRenderContext } from './app-render'
import { getAssetQueryString } from './get-asset-query-string'
export async function createComponentAndStyles({
export async function createComponentStylesAndScripts({
filePath,
getComponent,
injectedCSS,
injectedJS,
ctx,
}: {
filePath: string
getComponent: () => any
injectedCSS: Set<string>
injectedJS: Set<string>
ctx: AppRenderContext
}): Promise<any> {
const cssHrefs = getCssInlinedLinkTags(
}): Promise<[any, React.ReactNode, React.ReactNode]> {
const { styles: cssHrefs, scripts: jsHrefs } = getLinkAndScriptTags(
ctx.clientReferenceManifest,
filePath,
injectedCSS
injectedCSS,
injectedJS
)
const styles = cssHrefs
@ -56,7 +59,13 @@ export async function createComponentAndStyles({
})
: null
const scripts = jsHrefs
? jsHrefs.map((href) => (
<script src={`${ctx.assetPrefix}/_next/${href}`} async={true} />
))
: null
const Comp = interopDefault(await getComponent())
return [Comp, styles]
return [Comp, styles, scripts]
}

View file

@ -8,7 +8,7 @@ import { preloadComponent } from './preload-component'
import { addSearchParamsIfPageSegment } from './create-flight-router-state-from-loader-tree'
import { parseLoaderTree } from './parse-loader-tree'
import type { CreateSegmentPath, AppRenderContext } from './app-render'
import { createComponentAndStyles } from './create-component-and-styles'
import { createComponentStylesAndScripts } from './create-component-styles-and-scripts'
import { getLayerAssets } from './get-layer-assets'
import { hasLoadingComponentInTree } from './has-loading-component-in-tree'
@ -23,6 +23,7 @@ export async function createComponentTree({
firstItem,
rootLayoutIncluded,
injectedCSS,
injectedJS,
injectedFontPreloadTags,
asNotFound,
metadataOutlet,
@ -34,6 +35,7 @@ export async function createComponentTree({
rootLayoutIncluded: boolean
firstItem?: boolean
injectedCSS: Set<string>
injectedJS: Set<string>
injectedFontPreloadTags: Set<string>
asNotFound?: boolean
metadataOutlet?: React.ReactNode
@ -66,41 +68,46 @@ export async function createComponentTree({
const { layout, template, error, loading, 'not-found': notFound } = components
const injectedCSSWithCurrentLayout = new Set(injectedCSS)
const injectedJSWithCurrentLayout = new Set(injectedJS)
const injectedFontPreloadTagsWithCurrentLayout = new Set(
injectedFontPreloadTags
)
const styles = getLayerAssets({
const layerAssets = getLayerAssets({
ctx,
layoutOrPagePath,
injectedCSS: injectedCSSWithCurrentLayout,
injectedJS: injectedJSWithCurrentLayout,
injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout,
})
const [Template, templateStyles] = template
? await createComponentAndStyles({
const [Template, templateStyles, templateScripts] = template
? await createComponentStylesAndScripts({
ctx,
filePath: template[1],
getComponent: template[0],
injectedCSS: injectedCSSWithCurrentLayout,
injectedJS: injectedJSWithCurrentLayout,
})
: [React.Fragment]
const [ErrorComponent, errorStyles] = error
? await createComponentAndStyles({
const [ErrorComponent, errorStyles, errorScripts] = error
? await createComponentStylesAndScripts({
ctx,
filePath: error[1],
getComponent: error[0],
injectedCSS: injectedCSSWithCurrentLayout,
injectedJS: injectedJSWithCurrentLayout,
})
: []
const [Loading, loadingStyles] = loading
? await createComponentAndStyles({
const [Loading, loadingStyles, loadingScripts] = loading
? await createComponentStylesAndScripts({
ctx,
filePath: loading[1],
getComponent: loading[0],
injectedCSS: injectedCSSWithCurrentLayout,
injectedJS: injectedJSWithCurrentLayout,
})
: []
@ -119,11 +126,12 @@ export async function createComponentTree({
rootLayoutIncluded || rootLayoutAtThisLevel
const [NotFound, notFoundStyles] = notFound
? await createComponentAndStyles({
? await createComponentStylesAndScripts({
ctx,
filePath: notFound[1],
getComponent: notFound[0],
injectedCSS: injectedCSSWithCurrentLayout,
injectedJS: injectedJSWithCurrentLayout,
})
: []
@ -210,7 +218,7 @@ export async function createComponentTree({
<NotFoundBoundary
notFound={
<>
{styles}
{layerAssets}
<RootLayoutComponent>
{notFoundStyles}
<NotFoundComponent />
@ -302,16 +310,19 @@ export async function createComponentTree({
segmentPath={createSegmentPath(currentSegmentPath)}
loading={Loading ? <Loading /> : undefined}
loadingStyles={loadingStyles}
loadingScripts={loadingScripts}
// TODO-APP: Add test for loading returning `undefined`. This currently can't be tested as the `webdriver()` tab will wait for the full page to load before returning.
hasLoading={Boolean(Loading)}
error={ErrorComponent}
errorStyles={errorStyles}
errorScripts={errorScripts}
template={
<Template>
<RenderFromTemplateContext />
</Template>
}
templateStyles={templateStyles}
templateScripts={templateScripts}
notFound={notFoundComponent}
notFoundStyles={notFoundStyles}
childProp={currentChildProp}
@ -345,6 +356,7 @@ export async function createComponentTree({
parentParams: currentParams,
rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove,
injectedCSS: injectedCSSWithCurrentLayout,
injectedJS: injectedJSWithCurrentLayout,
injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout,
asNotFound,
metadataOutlet,
@ -378,7 +390,7 @@ export async function createComponentTree({
if (!Component) {
return {
Component: () => <>{parallelRouteComponents.children}</>,
styles,
styles: layerAssets,
}
}
@ -461,6 +473,6 @@ export async function createComponentTree({
</>
)
},
styles,
styles: layerAssets,
}
}

View file

@ -3,28 +3,43 @@ import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight
/**
* Get external stylesheet link hrefs based on server CSS manifest.
*/
export function getCssInlinedLinkTags(
export function getLinkAndScriptTags(
clientReferenceManifest: ClientReferenceManifest,
filePath: string,
injectedCSS: Set<string>,
collectNewCSSImports?: boolean
): string[] {
injectedScripts: Set<string>,
collectNewImports?: boolean
): { styles: string[]; scripts: string[] } {
const filePathWithoutExt = filePath.replace(/\.[^.]+$/, '')
const chunks = new Set<string>()
const cssChunks = new Set<string>()
const jsChunks = new Set<string>()
const entryCSSFiles =
clientReferenceManifest.entryCSSFiles[filePathWithoutExt]
const entryJSFiles =
clientReferenceManifest.entryJSFiles?.[filePathWithoutExt] ?? []
if (entryCSSFiles) {
for (const file of entryCSSFiles) {
if (!injectedCSS.has(file)) {
if (collectNewCSSImports) {
if (collectNewImports) {
injectedCSS.add(file)
}
chunks.add(file)
cssChunks.add(file)
}
}
}
return [...chunks]
if (entryJSFiles) {
for (const file of entryJSFiles) {
if (!injectedScripts.has(file)) {
if (collectNewImports) {
injectedScripts.add(file)
}
jsChunks.add(file)
}
}
}
return { styles: [...cssChunks], scripts: [...jsChunks] }
}

View file

@ -1,5 +1,5 @@
import React from 'react'
import { getCssInlinedLinkTags } from './get-css-inlined-link-tags'
import { getLinkAndScriptTags } from './get-css-inlined-link-tags'
import { getPreloadableFonts } from './get-preloadable-fonts'
import type { AppRenderContext } from './app-render'
import { getAssetQueryString } from './get-asset-query-string'
@ -8,21 +8,24 @@ export function getLayerAssets({
ctx,
layoutOrPagePath,
injectedCSS: injectedCSSWithCurrentLayout,
injectedJS: injectedJSWithCurrentLayout,
injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout,
}: {
layoutOrPagePath: string | undefined
injectedCSS: Set<string>
injectedJS: Set<string>
injectedFontPreloadTags: Set<string>
ctx: AppRenderContext
}): React.ReactNode {
const stylesheets: string[] = layoutOrPagePath
? getCssInlinedLinkTags(
const { styles: styleTags, scripts: scriptTags } = layoutOrPagePath
? getLinkAndScriptTags(
ctx.clientReferenceManifest,
layoutOrPagePath,
injectedCSSWithCurrentLayout,
injectedJSWithCurrentLayout,
true
)
: []
: { styles: [], scripts: [] }
const preloadedFontFiles = layoutOrPagePath
? getPreloadableFonts(
@ -53,8 +56,8 @@ export function getLayerAssets({
}
}
const styles = stylesheets
? stylesheets.map((href, index) => {
const styles = styleTags
? styleTags.map((href, index) => {
// In dev, Safari and Firefox will cache the resource during HMR:
// - https://github.com/vercel/next.js/issues/5860
// - https://bugs.webkit.org/show_bug.cgi?id=187726
@ -88,7 +91,15 @@ export function getLayerAssets({
/>
)
})
: null
: []
return styles
const scripts = scriptTags
? scriptTags.map((href, index) => {
const fullSrc = `${ctx.assetPrefix}/_next/${href}`
return <script src={fullSrc} async={true} key={index} />
})
: []
return styles.length || scripts.length ? [...styles, ...scripts] : null
}

View file

@ -10,7 +10,7 @@ import {
matchSegment,
} from '../../client/components/match-segments'
import type { LoaderTree } from '../lib/app-dir-module'
import { getCssInlinedLinkTags } from './get-css-inlined-link-tags'
import { getLinkAndScriptTags } from './get-css-inlined-link-tags'
import { getPreloadableFonts } from './get-preloadable-fonts'
import {
addSearchParamsIfPageSegment,
@ -35,6 +35,7 @@ export async function walkTreeWithFlightRouterState({
parentRendered,
rscPayloadHead,
injectedCSS,
injectedJS,
injectedFontPreloadTags,
rootLayoutIncluded,
asNotFound,
@ -49,6 +50,7 @@ export async function walkTreeWithFlightRouterState({
parentRendered?: boolean
rscPayloadHead: React.ReactNode
injectedCSS: Set<string>
injectedJS: Set<string>
injectedFontPreloadTags: Set<string>
rootLayoutIncluded: boolean
asNotFound?: boolean
@ -145,6 +147,7 @@ export async function walkTreeWithFlightRouterState({
parentParams: currentParams,
firstItem: isFirst,
injectedCSS,
injectedJS,
injectedFontPreloadTags,
// This is intentionally not "rootLayoutIncludedAtThisLevelOrAbove" as createComponentTree starts at the current level and does a check for "rootLayoutAtThisLevel" too.
rootLayoutIncluded,
@ -160,16 +163,17 @@ export async function walkTreeWithFlightRouterState({
: (() => {
const { layoutOrPagePath } = parseLoaderTree(loaderTreeToFilter)
const styles = getLayerAssets({
const layerAssets = getLayerAssets({
ctx,
layoutOrPagePath,
injectedCSS: new Set(injectedCSS),
injectedJS: new Set(injectedJS),
injectedFontPreloadTags: new Set(injectedFontPreloadTags),
})
return (
<>
{styles}
{layerAssets}
{rscPayloadHead}
</>
)
@ -183,14 +187,16 @@ export async function walkTreeWithFlightRouterState({
// the result consistent.
const layoutPath = layout?.[1]
const injectedCSSWithCurrentLayout = new Set(injectedCSS)
const injectedJSWithCurrentLayout = new Set(injectedJS)
const injectedFontPreloadTagsWithCurrentLayout = new Set(
injectedFontPreloadTags
)
if (layoutPath) {
getCssInlinedLinkTags(
getLinkAndScriptTags(
ctx.clientReferenceManifest,
layoutPath,
injectedCSSWithCurrentLayout,
injectedJSWithCurrentLayout,
true
)
getPreloadableFonts(
@ -224,6 +230,7 @@ export async function walkTreeWithFlightRouterState({
isFirst: false,
rscPayloadHead,
injectedCSS: injectedCSSWithCurrentLayout,
injectedJS: injectedJSWithCurrentLayout,
injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout,
rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove,
asNotFound,