Skip to content

Commit

Permalink
Handle dynamic css-in-js styles under suspense (#42293)
Browse files Browse the repository at this point in the history
We insert the content returning from `useServerInsertedHTML` to head
element for app dir, but it shouldn't only inject once since there're
suspense boundaries that could hold insertions.

## Bug

- [ ] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

Co-authored-by: JJ Kasper <jj@jjsweb.site>
  • Loading branch information
huozhi and ijjk committed Nov 2, 2022
1 parent 66dc68f commit 6dbf9c3
Show file tree
Hide file tree
Showing 13 changed files with 1,324 additions and 163 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -196,7 +196,7 @@
"selenium-webdriver": "4.0.0-beta.4",
"semver": "7.3.7",
"shell-quote": "1.7.3",
"styled-components": "5.3.3",
"styled-components": "6.0.0-beta.5",
"styled-jsx-plugin-postcss": "3.0.2",
"swr": "2.0.0-rc.0",
"tailwindcss": "1.1.3",
Expand Down
47 changes: 36 additions & 11 deletions packages/next/server/node-web-streams-helper.ts
@@ -1,6 +1,9 @@
import type { FlightRouterState } from './app-render'
import { nonNullable } from '../lib/non-nullable'

const queueTask =
process.env.NEXT_RUNTIME === 'edge' ? globalThis.setTimeout : setImmediate

export type ReactReadableStream = ReadableStream<Uint8Array> & {
allReady?: Promise<void> | undefined
}
Expand Down Expand Up @@ -149,21 +152,43 @@ export function renderToInitialStream({
return ReactDOMServer.renderToReadableStream(element, streamOptions)
}

export function createHeadInjectionTransformStream(
inject: () => Promise<string>
function createHeadInsertionTransformStream(
insert: () => Promise<string>
): TransformStream<Uint8Array, Uint8Array> {
let injected = false
let inserted = false
let freezing = false

return new TransformStream({
async transform(chunk, controller) {
const content = decodeText(chunk)
let index
if (!injected && (index = content.indexOf('</head')) !== -1) {
injected = true
const injectedContent =
content.slice(0, index) + (await inject()) + content.slice(index)
controller.enqueue(encodeText(injectedContent))
// While react is flushing chunks, we don't apply insertions
if (freezing) {
controller.enqueue(chunk)
return
}

const insertion = await insert()
if (inserted) {
controller.enqueue(encodeText(insertion))
controller.enqueue(chunk)
freezing = true
} else {
const content = decodeText(chunk)
const index = content.indexOf('</head')
if (index !== -1) {
const insertedHeadContent =
content.slice(0, index) + insertion + content.slice(index)
controller.enqueue(encodeText(insertedHeadContent))
freezing = true
inserted = true
}
}

if (!inserted) {
controller.enqueue(chunk)
} else {
queueTask(() => {
freezing = false
})
}
},
})
Expand Down Expand Up @@ -333,7 +358,7 @@ export async function continueFromInitialStream(
suffixUnclosed != null ? createDeferredSuffixStream(suffixUnclosed) : null,
dataStream ? createInlineDataStream(dataStream) : null,
suffixUnclosed != null ? createSuffixStream(closeTag) : null,
createHeadInjectionTransformStream(async () => {
createHeadInsertionTransformStream(async () => {
// TODO-APP: Insert server side html to end of head in app layout rendering, to avoid
// hydration errors. Remove this once it's ready to be handled by react itself.
const serverInsertedHTML =
Expand Down

0 comments on commit 6dbf9c3

Please sign in to comment.