From 62b2a16ab369f306be20b26fe754fd2280a59ac8 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 12 Jul 2022 14:07:50 +0200 Subject: [PATCH 1/5] Handle dev fouc for layout styling --- packages/next/client/app-index.tsx | 90 ++++++++++++------- packages/next/client/app-next-dev.js | 3 +- packages/next/client/dev/fouc.ts | 1 + packages/next/server/app-render.tsx | 25 +++--- .../next/server/node-web-streams-helper.ts | 80 +++++++++++++---- 5 files changed, 137 insertions(+), 62 deletions(-) diff --git a/packages/next/client/app-index.tsx b/packages/next/client/app-index.tsx index dd11f6a8d4b0..4245de9a185f 100644 --- a/packages/next/client/app-index.tsx +++ b/packages/next/client/app-index.tsx @@ -143,35 +143,29 @@ function createResponseCache() { } const rscCache = createResponseCache() -function useInitialServerResponse(cacheKey: string) { - const response = rscCache.get(cacheKey) - if (response) return response - - const readable = new ReadableStream({ - start(controller) { - nextServerDataRegisterWriter(controller) - }, - }) - - async function loadCss(cssChunkInfoJson: string) { - const data = JSON.parse(cssChunkInfoJson) - await Promise.all( - data.chunks.map((chunkId: string) => { - // load css related chunks - return (self as any).__next_chunk_load__(chunkId) - }) - ) - // In development mode, import css in dev when it's wrapped by style loader. - // In production mode, css are standalone chunk that doesn't need to be imported. - if (data.id) { - ;(self as any).__next_require__(data.id) - } +async function loadCss(cssChunkInfoJson: string) { + const data = JSON.parse(cssChunkInfoJson) + await Promise.all( + data.chunks.map((chunkId: string) => { + // load css related chunks + return (self as any).__next_chunk_load__(chunkId) + }) + ) + // In development mode, import css in dev when it's wrapped by style loader. + // In production mode, css are standalone chunk that doesn't need to be imported. + if (data.id) { + return (self as any).__next_require__(data.id) } + return Promise.resolve(1) +} + +function createLoadFlightCssStream(callback?: () => Promise) { + const promises: Promise[] = [] const loadCssFromStreamData = (data: string) => { const seg = data.split(':') if (seg[0] === 'CSS') { - loadCss(seg.slice(1).join(':')) + promises.push(loadCss(seg.slice(1).join(':'))) } } @@ -186,28 +180,54 @@ function useInitialServerResponse(cacheKey: string) { buffer = buffer.slice(index + 1) loadCssFromStreamData(line) } + loadCssFromStreamData(buffer) if (!data.startsWith('CSS:')) { controller.enqueue(chunk) } }, - flush() { - loadCssFromStreamData(buffer) + }) + + if (callback) { + Promise.all(promises).then(() => { + // TODO: find better timing for css injection + setTimeout(() => { + callback() + }) + }) + } + + return loadCssFromFlight +} + +function useInitialServerResponse(cacheKey: string, onFlightCssLoaded: any) { + const response = rscCache.get(cacheKey) + if (response) return response + + const readable = new ReadableStream({ + start(controller) { + nextServerDataRegisterWriter(controller) }, }) const newResponse = createFromReadableStream( - readable.pipeThrough(loadCssFromFlight) + readable.pipeThrough(createLoadFlightCssStream(onFlightCssLoaded)) ) rscCache.set(cacheKey, newResponse) return newResponse } -function ServerRoot({ cacheKey }: { cacheKey: string }) { +function ServerRoot({ + cacheKey, + onFlightCssLoaded, +}: { + cacheKey: string + onFlightCssLoaded: any +}) { React.useEffect(() => { rscCache.delete(cacheKey) }) - const response = useInitialServerResponse(cacheKey) + const response = useInitialServerResponse(cacheKey, onFlightCssLoaded) const root = response.readRoot() return root } @@ -227,16 +247,20 @@ function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement { return children as React.ReactElement } -function RSCComponent() { +function RSCComponent({ onFlightCssLoaded }: { onFlightCssLoaded: any }) { const cacheKey = getCacheKey() - return + return ( + + ) } -export function hydrate() { +export async function hydrate(opts: { + onFlightCssLoaded?: () => Promise +}) { renderReactElement(appElement!, () => ( - + )) diff --git a/packages/next/client/app-next-dev.js b/packages/next/client/app-next-dev.js index 1ab8210e8337..be6e3f10007b 100644 --- a/packages/next/client/app-next-dev.js +++ b/packages/next/client/app-next-dev.js @@ -1,4 +1,5 @@ import { hydrate, version } from './app-index' +import { displayContent } from './dev/fouc' // TODO-APP: implement FOUC guard @@ -9,6 +10,6 @@ window.next = { appDir: true, } -hydrate() +hydrate({ onFlightCssLoaded: displayContent }) // TODO-APP: build indicator diff --git a/packages/next/client/dev/fouc.ts b/packages/next/client/dev/fouc.ts index bca480e22719..2691c887d7de 100644 --- a/packages/next/client/dev/fouc.ts +++ b/packages/next/client/dev/fouc.ts @@ -6,6 +6,7 @@ const safeSetTimeout = (callback: () => void) => setTimeout(callback) // rendering won't have the correct computed values in effects. export function displayContent(): Promise { return new Promise((resolve) => { + console.log('fouc') ;(window.requestAnimationFrame || safeSetTimeout)(function () { for ( var x = document.querySelectorAll('[data-next-hide-fouc]'), diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 898dd59c848f..66d87a58e5f3 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -131,6 +131,7 @@ function useFlightResponse( .pipeThrough(createPrefixStream(cssFlightData)) .getReader() const writer = writable.getWriter() + // let remainingFlightResponse = '' function process() { forwardReader.read().then(({ done, value }) => { if (!bootstrapped) { @@ -148,13 +149,11 @@ function useFlightResponse( writer.close() } else { const responsePartial = decodeText(value) - writer.write( - encodeText( - `` - ) - ) + const scripts = `` + + writer.write(encodeText(scripts)) process() } }) @@ -335,20 +334,19 @@ 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)}\n`).join('') } // Multiple css chunks could be merged into one by mini-css-extract-plugin, // we use a set here to dedupe the css chunks in production. - const cssSet = cssFiles.reduce((res, css) => { + const cssSet: Set = cssFiles.reduce((res, css) => { res.add(...css.chunks) return res }, new Set()) - const cssFlight = Array.from(cssSet) - .map((css) => `CSS:${JSON.stringify({ chunks: [css] })}`) - .join('\n') - return cssFlight + return cssSet.size + ? `CSS:${JSON.stringify({ chunks: [Array.from(cssSet)] })}\n` + : '' } export async function renderToHTML( @@ -896,6 +894,7 @@ export async function renderToHTML( } return await continueFromInitialStream(renderStream, { + dev: renderOpts.dev, suffix: '', dataStream: serverComponentsInlinedTransformStream?.readable, generateStaticHTML: generateStaticHTML || !hasConcurrentFeatures, diff --git a/packages/next/server/node-web-streams-helper.ts b/packages/next/server/node-web-streams-helper.ts index 77808517b8f5..c512630ea47c 100644 --- a/packages/next/server/node-web-streams-helper.ts +++ b/packages/next/server/node-web-streams-helper.ts @@ -136,6 +136,31 @@ export function createFlushEffectStream( }) } +export function createDevScriptTransformStream(): TransformStream< + Uint8Array, + Uint8Array +> { + return new TransformStream({ + transform(chunk, controller) { + const content = decodeText(chunk) + + if (content.includes('')) { + const injectedContent = content.replaceAll( + '', + ` + +` + ) + controller.enqueue(encodeText(injectedContent)) + } else { + controller.enqueue(chunk) + } + }, + }) +} + export function renderToInitialStream({ ReactDOMServer, element, @@ -151,11 +176,13 @@ export function renderToInitialStream({ export async function continueFromInitialStream( renderStream: ReactReadableStream, { + dev, suffix, dataStream, generateStaticHTML, flushEffectHandler, }: { + dev?: boolean suffix?: string dataStream?: ReadableStream generateStaticHTML: boolean @@ -172,9 +199,10 @@ export async function continueFromInitialStream( const transforms: Array> = [ createBufferedTransformStream(), flushEffectHandler ? createFlushEffectStream(flushEffectHandler) : null, - suffixUnclosed != null ? createPrefixStream(suffixUnclosed) : null, + suffixUnclosed != null ? createBufferedSuffix(suffixUnclosed) : null, dataStream ? createInlineDataStream(dataStream) : null, suffixUnclosed != null ? createSuffixStream(closeTag) : null, + dev ? createDevScriptTransformStream() : null, ].filter(nonNullable) return transforms.reduce( @@ -183,6 +211,26 @@ export async function continueFromInitialStream( ) } +export function createPrefixStream( + prefix: string +): TransformStream { + let prefixFlushed = false + return new TransformStream({ + transform(chunk, controller) { + if (!prefixFlushed) { + prefixFlushed = true + controller.enqueue(encodeText(prefix)) + } + controller.enqueue(chunk) + }, + flush(controller) { + if (!prefixFlushed) { + controller.enqueue(encodeText(prefix)) + } + }, + }) +} + export function createSuffixStream( suffix: string ): TransformStream { @@ -195,32 +243,34 @@ export function createSuffixStream( }) } -export function createPrefixStream( - prefix: string +// Suffix after main body content - scripts before , +// but wait for the major chunks to be enqueued. +export function createBufferedSuffix( + suffix: string ): TransformStream { - let prefixFlushed = false - let prefixPrefixFlushFinished: Promise | null = null + let suffixFlushed = false + let suffixFlushTask: Promise | null = null return new TransformStream({ transform(chunk, controller) { controller.enqueue(chunk) - if (!prefixFlushed && prefix) { - prefixFlushed = true - prefixPrefixFlushFinished = new Promise((res) => { + if (!suffixFlushed && suffix) { + suffixFlushed = true + suffixFlushTask = new Promise((res) => { // NOTE: streaming flush - // Enqueue prefix part before the major chunks are enqueued so that - // prefix won't be flushed too early to interrupt the data stream + // Enqueue suffix part before the major chunks are enqueued so that + // suffix won't be flushed too early to interrupt the data stream setTimeout(() => { - controller.enqueue(encodeText(prefix)) + controller.enqueue(encodeText(suffix)) res() }) }) } }, flush(controller) { - if (prefixPrefixFlushFinished) return prefixPrefixFlushFinished - if (!prefixFlushed && prefix) { - prefixFlushed = true - controller.enqueue(encodeText(prefix)) + if (suffixFlushTask) return suffixFlushTask + if (!suffixFlushed && suffix) { + suffixFlushed = true + controller.enqueue(encodeText(suffix)) } }, }) From 6b25aa2e21485d730ec0b6bffdd6cffe496a12d1 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 12 Jul 2022 16:31:54 +0200 Subject: [PATCH 2/5] refactor --- packages/next/client/app-index.tsx | 4 ++-- packages/next/server/app-render.tsx | 16 ++++++++++------ packages/next/server/node-web-streams-helper.ts | 15 ++++++++------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/next/client/app-index.tsx b/packages/next/client/app-index.tsx index 4245de9a185f..2e4c91251000 100644 --- a/packages/next/client/app-index.tsx +++ b/packages/next/client/app-index.tsx @@ -254,13 +254,13 @@ function RSCComponent({ onFlightCssLoaded }: { onFlightCssLoaded: any }) { ) } -export async function hydrate(opts: { +export async function hydrate(opts?: { onFlightCssLoaded?: () => Promise }) { renderReactElement(appElement!, () => ( - + )) diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 66d87a58e5f3..14cc529260f8 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -326,14 +326,19 @@ function getSegmentParam(segment: string): { return null } -function getCssFlightData(ComponentMod: any, serverComponentManifest: any) { +function getCssFlightData( + ComponentMod: any, + serverComponentManifest: any, + dev?: boolean +) { const importedServerCSSFiles: string[] = ComponentMod.__client__?.__next_rsc_css__ || [] const cssFiles = importedServerCSSFiles.map( (css) => serverComponentManifest[css].default ) - if (process.env.NODE_ENV === 'development') { + + if (dev) { return cssFiles.map((css) => `CSS:${JSON.stringify(css)}\n`).join('') } @@ -344,9 +349,7 @@ function getCssFlightData(ComponentMod: any, serverComponentManifest: any) { return res }, new Set()) - return cssSet.size - ? `CSS:${JSON.stringify({ chunks: [Array.from(cssSet)] })}\n` - : '' + return cssSet.size ? `CSS:${JSON.stringify({ chunks: [...cssSet] })}\n` : '' } export async function renderToHTML( @@ -765,7 +768,8 @@ export async function renderToHTML( const cssFlightData = getCssFlightData( ComponentMod, - serverComponentManifest + serverComponentManifest, + renderOpts.dev ) const flightData: FlightData = [ // TODO-APP: change walk to output without '' diff --git a/packages/next/server/node-web-streams-helper.ts b/packages/next/server/node-web-streams-helper.ts index c512630ea47c..c9569caf444d 100644 --- a/packages/next/server/node-web-streams-helper.ts +++ b/packages/next/server/node-web-streams-helper.ts @@ -140,18 +140,19 @@ export function createDevScriptTransformStream(): TransformStream< Uint8Array, Uint8Array > { + const headClosedTag = '' + const foucTags = ` + ` return new TransformStream({ transform(chunk, controller) { const content = decodeText(chunk) - if (content.includes('')) { + if (content.includes(headClosedTag)) { const injectedContent = content.replaceAll( - '', - ` - -` + headClosedTag, + foucTags + headClosedTag ) controller.enqueue(encodeText(injectedContent)) } else { From 1d5cab24abfe9574915e46f09923a5dd52e9eb89 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 12 Jul 2022 16:52:22 +0200 Subject: [PATCH 3/5] fix renderOpts.dev --- packages/next/client/app-index.tsx | 4 ++-- packages/next/client/dev/fouc.ts | 1 - packages/next/server/app-render.tsx | 19 +++++++++++++------ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/next/client/app-index.tsx b/packages/next/client/app-index.tsx index 2e4c91251000..74b0188408dc 100644 --- a/packages/next/client/app-index.tsx +++ b/packages/next/client/app-index.tsx @@ -187,11 +187,11 @@ function createLoadFlightCssStream(callback?: () => Promise) { }, }) - if (callback) { + if (process.env.NODE_ENV === 'development') { Promise.all(promises).then(() => { // TODO: find better timing for css injection setTimeout(() => { - callback() + callback?.() }) }) } diff --git a/packages/next/client/dev/fouc.ts b/packages/next/client/dev/fouc.ts index 2691c887d7de..bca480e22719 100644 --- a/packages/next/client/dev/fouc.ts +++ b/packages/next/client/dev/fouc.ts @@ -6,7 +6,6 @@ const safeSetTimeout = (callback: () => void) => setTimeout(callback) // rendering won't have the correct computed values in effects. export function displayContent(): Promise { return new Promise((resolve) => { - console.log('fouc') ;(window.requestAnimationFrame || safeSetTimeout)(function () { for ( var x = document.querySelectorAll('[data-next-hide-fouc]'), diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 14cc529260f8..f073155f3953 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -177,7 +177,8 @@ function createServerComponentRenderer( transformStream: TransformStream serverComponentManifest: NonNullable serverContexts: Array<[ServerContextName: string, JSONValue: any]> - } + }, + dev: boolean ) { // We need to expose the `__webpack_require__` API globally for // react-server-dom-webpack. This is a hack until we find a better way. @@ -191,7 +192,11 @@ function createServerComponentRenderer( globalThis.__next_chunk_load__ = () => Promise.resolve() } - const cssFlightData = getCssFlightData(ComponentMod, serverComponentManifest) + const cssFlightData = getCssFlightData( + ComponentMod, + serverComponentManifest, + dev + ) let RSCStream: ReadableStream const createRSCStream = () => { @@ -329,7 +334,7 @@ function getSegmentParam(segment: string): { function getCssFlightData( ComponentMod: any, serverComponentManifest: any, - dev?: boolean + dev: boolean ) { const importedServerCSSFiles: string[] = ComponentMod.__client__?.__next_rsc_css__ || [] @@ -370,6 +375,7 @@ export async function renderToHTML( runtime, ComponentMod, } = renderOpts + const dev = !!renderOpts.dev const isFlight = query.__flight__ !== undefined @@ -769,7 +775,7 @@ export async function renderToHTML( const cssFlightData = getCssFlightData( ComponentMod, serverComponentManifest, - renderOpts.dev + dev ) const flightData: FlightData = [ // TODO-APP: change walk to output without '' @@ -848,7 +854,8 @@ export async function renderToHTML( transformStream: serverComponentsInlinedTransformStream, serverComponentManifest, serverContexts, - } + }, + dev ) const jsxStyleRegistry = createStyleRegistry() @@ -898,7 +905,7 @@ export async function renderToHTML( } return await continueFromInitialStream(renderStream, { - dev: renderOpts.dev, + dev, suffix: '', dataStream: serverComponentsInlinedTransformStream?.readable, generateStaticHTML: generateStaticHTML || !hasConcurrentFeatures, From 4280fccb2a04e2136c58579eacbdfed091ca0b04 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 12 Jul 2022 19:19:28 +0200 Subject: [PATCH 4/5] dedupe css loading --- packages/next/client/app-index.tsx | 14 +++++++------- packages/next/server/app-render.tsx | 1 - .../next/server/node-web-streams-helper.ts | 19 ++++++++++--------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/next/client/app-index.tsx b/packages/next/client/app-index.tsx index 74b0188408dc..715f257db3d0 100644 --- a/packages/next/client/app-index.tsx +++ b/packages/next/client/app-index.tsx @@ -72,6 +72,7 @@ const getCacheKey = () => { } const encoder = new TextEncoder() +const loadedCss: Set = new Set() let initialServerDataBuffer: string[] | undefined = undefined let initialServerDataWriter: ReadableStreamDefaultController | undefined = @@ -161,11 +162,12 @@ async function loadCss(cssChunkInfoJson: string) { } function createLoadFlightCssStream(callback?: () => Promise) { - const promises: Promise[] = [] + const cssLoadingPromises: Promise[] = [] const loadCssFromStreamData = (data: string) => { const seg = data.split(':') if (seg[0] === 'CSS') { - promises.push(loadCss(seg.slice(1).join(':'))) + const cssJson = seg.slice(1).join(':') + if (!loadedCss.has(cssJson)) cssLoadingPromises.push(loadCss(cssJson)) } } @@ -188,7 +190,7 @@ function createLoadFlightCssStream(callback?: () => Promise) { }) if (process.env.NODE_ENV === 'development') { - Promise.all(promises).then(() => { + Promise.all(cssLoadingPromises).then(() => { // TODO: find better timing for css injection setTimeout(() => { callback?.() @@ -247,11 +249,9 @@ function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement { return children as React.ReactElement } -function RSCComponent({ onFlightCssLoaded }: { onFlightCssLoaded: any }) { +function RSCComponent(props: any) { const cacheKey = getCacheKey() - return ( - - ) + return } export async function hydrate(opts?: { diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index f073155f3953..0f391dc7e9ec 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -131,7 +131,6 @@ function useFlightResponse( .pipeThrough(createPrefixStream(cssFlightData)) .getReader() const writer = writable.getWriter() - // let remainingFlightResponse = '' function process() { forwardReader.read().then(({ done, value }) => { if (!bootstrapped) { diff --git a/packages/next/server/node-web-streams-helper.ts b/packages/next/server/node-web-streams-helper.ts index c9569caf444d..ddbdedce2f60 100644 --- a/packages/next/server/node-web-streams-helper.ts +++ b/packages/next/server/node-web-streams-helper.ts @@ -140,20 +140,21 @@ export function createDevScriptTransformStream(): TransformStream< Uint8Array, Uint8Array > { - const headClosedTag = '' + let injected = false const foucTags = ` - ` + ` return new TransformStream({ transform(chunk, controller) { const content = decodeText(chunk) + let index + if (!injected && (index = content.indexOf(' Date: Wed, 13 Jul 2022 00:39:10 +0200 Subject: [PATCH 5/5] keep css module id in dev --- packages/next/client/app-index.tsx | 32 +++++++++++-------- .../client/components/app-router.client.tsx | 26 +++++++-------- packages/next/server/app-render.tsx | 20 ++++++++++-- .../next/server/node-web-streams-helper.ts | 4 +-- 4 files changed, 50 insertions(+), 32 deletions(-) diff --git a/packages/next/client/app-index.tsx b/packages/next/client/app-index.tsx index 2e51e8277210..b7ca2b3d227a 100644 --- a/packages/next/client/app-index.tsx +++ b/packages/next/client/app-index.tsx @@ -169,7 +169,10 @@ function createLoadFlightCssStream(callback?: () => Promise) { const loadCssFromStreamData = (data: string) => { if (data.startsWith('CSS')) { const cssJson = data.slice(4).trim() - if (!loadedCss.has(cssJson)) cssLoadingPromises.push(loadCss(cssJson)) + if (!loadedCss.has(cssJson)) { + loadedCss.add(cssJson) + cssLoadingPromises.push(loadCss(cssJson)) + } } } @@ -177,23 +180,26 @@ function createLoadFlightCssStream(callback?: () => Promise) { let buffer = '' const loadCssFromFlight = new TransformStream({ transform(chunk, controller) { + const process = (buf: string) => { + if (buf) { + if (buf.startsWith('CSS:')) { + loadCssFromStreamData(buf) + } else { + controller.enqueue(new TextEncoder().encode(buf)) + } + } + } + const data = new TextDecoder().decode(chunk) buffer += data let index while ((index = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, index + 1) buffer = buffer.slice(index + 1) - if (line.startsWith('CSS:')) { - loadCssFromStreamData(line) - } else { - controller.enqueue(new TextEncoder().encode(line)) - } - } - - if (buffer && !buffer.startsWith('CSS:')) { - controller.enqueue(new TextEncoder().encode(buffer)) - buffer = '' + process(line) } + process(buffer) + buffer = '' }, }) @@ -262,9 +268,7 @@ function RSCComponent(props: any) { return } -export async function hydrate(opts?: { - onFlightCssLoaded?: () => Promise -}) { +export function hydrate(opts?: { onFlightCssLoaded?: () => Promise }) { renderReactElement(appElement!, () => ( diff --git a/packages/next/client/components/app-router.client.tsx b/packages/next/client/components/app-router.client.tsx index 9b571472108e..cd9d5b06a239 100644 --- a/packages/next/client/components/app-router.client.tsx +++ b/packages/next/client/components/app-router.client.tsx @@ -49,26 +49,26 @@ function fetchFlight(url: URL, flightRouterStateData: string): ReadableStream { let buffer = '' const loadCssFromFlight = new TransformStream({ transform(chunk, controller) { + const process = (buf: string) => { + if (buf) { + if (buf.startsWith('CSS:')) { + loadCssFromStreamData(buf) + } else { + controller.enqueue(new TextEncoder().encode(buf)) + } + } + } + const data = new TextDecoder().decode(chunk) buffer += data let index while ((index = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, index + 1) buffer = buffer.slice(index + 1) - - if (line.startsWith('CSS:')) { - loadCssFromStreamData(line) - } else { - controller.enqueue(new TextEncoder().encode(line)) - } - } - if (buffer && !buffer.startsWith('CSS:')) { - controller.enqueue(new TextEncoder().encode(buffer)) - buffer = '' + process(line) } - }, - flush() { - loadCssFromStreamData(buffer) + process(buffer) + buffer = '' }, }) diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index bd5109398f94..993495b9e6e6 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -191,7 +191,11 @@ function createServerComponentRenderer( globalThis.__next_chunk_load__ = () => Promise.resolve() } - const cssFlightData = getCssFlightData(ComponentMod, serverComponentManifest) + const cssFlightData = getCssFlightData( + ComponentMod, + serverComponentManifest, + dev + ) let RSCStream: ReadableStream const createRSCStream = () => { @@ -346,7 +350,11 @@ function getCSSInlinedLinkTags( ) } -function getCssFlightData(ComponentMod: any, serverComponentManifest: any) { +function getCssFlightData( + ComponentMod: any, + serverComponentManifest: any, + dev: boolean +) { const importedServerCSSFiles: string[] = ComponentMod.__client__?.__next_rsc_css__ || [] @@ -354,6 +362,11 @@ function getCssFlightData(ComponentMod: any, serverComponentManifest: any) { (css) => serverComponentManifest[css].default ) + if (dev) { + // Keep `id` in dev mode css flight to require the css module + return cssFiles.map((css) => `CSS:${JSON.stringify(css)}`).join('\n') + '\n' + } + // Multiple css chunks could be merged into one by mini-css-extract-plugin, // we use a set here to dedupe the css chunks in production. const cssSet: Set = cssFiles.reduce((res, css) => { @@ -781,7 +794,8 @@ export async function renderToHTML( const cssFlightData = getCssFlightData( ComponentMod, - serverComponentManifest + serverComponentManifest, + dev ) const flightData: FlightData = [ // TODO-APP: change walk to output without '' diff --git a/packages/next/server/node-web-streams-helper.ts b/packages/next/server/node-web-streams-helper.ts index 6502173af9ae..769df24adefd 100644 --- a/packages/next/server/node-web-streams-helper.ts +++ b/packages/next/server/node-web-streams-helper.ts @@ -231,9 +231,9 @@ export function createPrefixStream( let prefixFlushed = false return new TransformStream({ transform(chunk, controller) { - if (!prefixFlushed && prefix) { - prefixFlushed = true + if (!prefixFlushed) { controller.enqueue(encodeText(prefix)) + prefixFlushed = true } controller.enqueue(chunk) },