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 injecting CSS link tags before page content #38559

Merged
merged 7 commits into from Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 7 additions & 4 deletions packages/next/client/app-index.tsx
Expand Up @@ -32,10 +32,13 @@ self.__next_require__ = __webpack_require__
// eslint-disable-next-line no-undef
;(self as any).__next_chunk_load__ = (chunk: string) => {
if (chunk.endsWith('.css')) {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = '/_next/' + chunk
document.head.appendChild(link)
const existingTag = document.querySelector(`link[href="${chunk}"]`)
if (!existingTag) {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = '/_next/' + chunk
document.head.appendChild(link)
}
return Promise.resolve()
}

Expand Down
3 changes: 3 additions & 0 deletions packages/next/client/components/app-router.client.tsx
Expand Up @@ -56,11 +56,13 @@ let initialParallelRoutes: CacheNode['parallelRoutes'] =
export default function AppRouter({
initialTree,
initialCanonicalUrl,
initialStylesheets,
children,
hotReloader,
}: {
initialTree: FlightRouterState
initialCanonicalUrl: string
initialStylesheets: string[]
children: React.ReactNode
hotReloader?: React.ReactNode
}) {
Expand Down Expand Up @@ -259,6 +261,7 @@ export default function AppRouter({
// Root node always has `url`
// Provided in AppTreeContext to ensure it can be overwritten in layout-router
url: canonicalUrl,
stylesheets: initialStylesheets,
}}
>
<ErrorOverlay>{cache.subTreeData}</ErrorOverlay>
Expand Down
7 changes: 6 additions & 1 deletion packages/next/client/components/layout-router.client.tsx
Expand Up @@ -235,7 +235,7 @@ export default function OuterLayoutRouter({
childProp: ChildProp
loading: React.ReactNode | undefined
}) {
const { childNodes, tree, url } = useContext(AppTreeContext)
const { childNodes, tree, url, stylesheets } = useContext(AppTreeContext)

let childNodesForParallelRouter = childNodes.get(parallelRouterKey)
if (!childNodesForParallelRouter) {
Expand All @@ -256,6 +256,11 @@ export default function OuterLayoutRouter({

return (
<>
{stylesheets
? stylesheets.map((href) => (
<link rel="stylesheet" href={`/_next/${href}`} key={href} />
))
: null}
{preservedSegments.map((preservedSegment) => {
return (
<LoadingBoundary loading={loading} key={preservedSegment}>
Expand Down
41 changes: 30 additions & 11 deletions packages/next/server/app-render.tsx
Expand Up @@ -209,7 +209,7 @@ function createServerComponentRenderer(
}

const writable = transformStream.writable
const ServerComponentWrapper = () => {
return function ServerComponentWrapper() {
const reqStream = createRSCStream()
const response = useFlightResponse(
writable,
Expand All @@ -218,11 +218,8 @@ function createServerComponentRenderer(
serverComponentManifest,
cssFlightData
)
const root = response.readRoot()
return root
return response.readRoot()
}

return ServerComponentWrapper
}

type DynamicParamTypes = 'catchall' | 'optional-catchall' | 'dynamic'
Expand Down Expand Up @@ -327,6 +324,26 @@ function getSegmentParam(segment: string): {
return null
}

function getCSSInlinedLinkTags(
ComponentMod: any,
serverComponentManifest: any
) {
const importedServerCSSFiles: string[] =
ComponentMod.__client__?.__next_rsc_css__ || []

return Array.from(
new Set(
importedServerCSSFiles
.map((css) =>
css.endsWith('.css')
? serverComponentManifest[css].default.chunks
: []
)
.flat()
)
)
}

function getCssFlightData(ComponentMod: any, serverComponentManifest: any) {
const importedServerCSSFiles: string[] =
ComponentMod.__client__?.__next_rsc_css__ || []
Expand All @@ -335,7 +352,7 @@ function getCssFlightData(ComponentMod: any, serverComponentManifest: any) {
(css) => serverComponentManifest[css].default
)
if (process.env.NODE_ENV === 'development') {
return cssFiles.map((css) => `CSS:${JSON.stringify(css)}`).join('\n')
return cssFiles.map((css) => `CSS:${JSON.stringify(css)}`).join('\n') + '\n'
}

// Multiple css chunks could be merged into one by mini-css-extract-plugin,
Expand All @@ -345,10 +362,7 @@ function getCssFlightData(ComponentMod: any, serverComponentManifest: any) {
return res
}, new Set())

const cssFlight = Array.from(cssSet)
.map((css) => `CSS:${JSON.stringify({ chunks: [css] })}`)
.join('\n')
return cssFlight
return `CSS:${JSON.stringify({ chunks: [...cssSet] })}\n`
}

export async function renderToHTML(
Expand Down Expand Up @@ -793,6 +807,11 @@ export async function renderToHTML(
// /blog/[slug] /blog/hello-world -> ['children', 'blog', 'children', ['slug', 'hello-world']]
const initialTree = createFlightRouterStateFromLoaderTree(tree)

const initialStylesheets = getCSSInlinedLinkTags(
ComponentMod,
serverComponentManifest
)

const { Component: ComponentTree } = createComponentTree({
createSegmentPath: (child) => child,
tree,
Expand All @@ -818,6 +837,7 @@ export async function renderToHTML(
hotReloader={HotReloader && <HotReloader assetPrefix="" />}
initialCanonicalUrl={initialCanonicalUrl}
initialTree={initialTree}
initialStylesheets={initialStylesheets}
>
<ComponentTree />
</AppRouter>
Expand Down Expand Up @@ -896,7 +916,6 @@ export async function renderToHTML(
}

return await continueFromInitialStream(renderStream, {
suffix: '',
dataStream: serverComponentsInlinedTransformStream?.readable,
generateStaticHTML: generateStaticHTML || !hasConcurrentFeatures,
flushEffectHandler,
Expand Down
17 changes: 16 additions & 1 deletion packages/next/server/node-web-streams-helper.ts
Expand Up @@ -172,7 +172,7 @@ export async function continueFromInitialStream(
const transforms: Array<TransformStream<Uint8Array, Uint8Array>> = [
createBufferedTransformStream(),
flushEffectHandler ? createFlushEffectStream(flushEffectHandler) : null,
suffixUnclosed != null ? createPrefixStream(suffixUnclosed) : null,
suffixUnclosed != null ? createDeferredPrefixStream(suffixUnclosed) : null,
dataStream ? createInlineDataStream(dataStream) : null,
suffixUnclosed != null ? createSuffixStream(closeTag) : null,
].filter(nonNullable)
Expand All @@ -197,6 +197,21 @@ export function createSuffixStream(

export function createPrefixStream(
prefix: string
): TransformStream<Uint8Array, Uint8Array> {
let prefixEnqueued = false
return new TransformStream({
transform(chunk, controller) {
if (!prefixEnqueued) {
controller.enqueue(encodeText(prefix))
prefixEnqueued = true
}
controller.enqueue(chunk)
},
})
}

export function createDeferredPrefixStream(
prefix: string
): TransformStream<Uint8Array, Uint8Array> {
let prefixFlushed = false
let prefixPrefixFlushFinished: Promise<void> | null = null
Expand Down
1 change: 1 addition & 0 deletions packages/next/shared/lib/app-router-context.ts
Expand Up @@ -28,6 +28,7 @@ export const AppTreeContext = React.createContext<{
childNodes: CacheNode['parallelRoutes']
tree: FlightRouterState
url: string
stylesheets?: string[]
}>(null as any)
export const FullAppTreeContext = React.createContext<{
tree: FlightRouterState
Expand Down