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

Improved server CSS handling #39664

Merged
merged 6 commits into from Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -177,8 +177,8 @@
"react-17": "npm:react@17.0.2",
"react-dom": "18.2.0",
"react-dom-17": "npm:react-dom@17.0.2",
"react-dom-exp": "npm:react-dom@0.0.0-experimental-4cd788aef-20220630",
"react-exp": "npm:react@0.0.0-experimental-4cd788aef-20220630",
"react-dom-exp": "npm:react-dom@0.0.0-experimental-6ef466c68-20220816",
"react-exp": "npm:react@0.0.0-experimental-6ef466c68-20220816",
"react-ssr-prepass": "1.0.8",
"react-virtualized": "9.22.3",
"relay-compiler": "13.0.2",
Expand Down
35 changes: 25 additions & 10 deletions packages/next/build/webpack/config/blocks/css/index.ts
Expand Up @@ -275,16 +275,31 @@ export const css = curry(async function css(
}

if (ctx.isServer) {
fns.push(
loader({
oneOf: [
markRemovable({
test: [regexCssGlobal, regexSassGlobal],
use: require.resolve('next/dist/compiled/ignore-loader'),
}),
],
})
)
if (ctx.experimental.appDir && !ctx.isProduction) {
fns.push(
loader({
oneOf: [
markRemovable({
test: [regexCssGlobal, regexSassGlobal],
use: require.resolve(
'../../../loaders/next-flight-css-dev-loader'
),
}),
],
})
)
} else {
fns.push(
loader({
oneOf: [
markRemovable({
test: [regexCssGlobal, regexSassGlobal],
use: require.resolve('next/dist/compiled/ignore-loader'),
}),
],
})
)
}
} else {
fns.push(
loader({
Expand Down
Expand Up @@ -21,7 +21,11 @@ export default async function transformSource(this: any): Promise<string> {
requests
// Filter out css files on the server
.filter((request) => (isServer ? !request.endsWith('.css') : true))
.map((request) => `import(/* webpackMode: "eager" */ '${request}')`)
.map((request) =>
request.endsWith('.css')
? `(() => import(/* webpackMode: "lazy" */ '${request}'))`
: `import(/* webpackMode: "eager" */ '${request}')`
)
.join(';\n') +
`
export const __next_rsc__ = {
Expand Down
@@ -0,0 +1,16 @@
/**
* For server-side CSS imports, we need to ignore the actual module content but
* still trigger the hot-reloading diff mechanism. So here we put the content
* inside a comment.
*/

const NextServerCSSLoader = function (this: any, source: string | Buffer) {
this.cacheable && this.cacheable()

return `export default "${(typeof source === 'string'
? Buffer.from(source)
: source
).toString('hex')}"`
}

export default NextServerCSSLoader
101 changes: 33 additions & 68 deletions packages/next/server/app-render.tsx
Expand Up @@ -378,55 +378,26 @@ function getSegmentParam(segment: string): {
/**
* Get inline <link> tags based on server CSS manifest. Only used when rendering to HTML.
*/
// function getCssInlinedLinkTags(
// serverComponentManifest: FlightManifest,
// serverCSSManifest: FlightCSSManifest,
// filePath: string
// ): string[] {
// const layoutOrPageCss = serverCSSManifest[filePath]

// if (!layoutOrPageCss) {
// return []
// }

// const chunks = new Set<string>()
function getCssInlinedLinkTags(
serverComponentManifest: FlightManifest,
serverCSSManifest: FlightCSSManifest,
filePath: string
): string[] {
const layoutOrPageCss = serverCSSManifest[filePath]

// for (const css of layoutOrPageCss) {
// for (const chunk of serverComponentManifest[css].default.chunks) {
// chunks.add(chunk)
// }
// }
if (!layoutOrPageCss) {
return []
}

// return [...chunks]
// }
const chunks = new Set<string>()

/**
* Get inline <link> tags based on server CSS manifest. Only used when rendering to HTML.
*/
function getAllCssInlinedLinkTags(
serverComponentManifest: FlightManifest,
serverCSSManifest: FlightCSSManifest
): string[] {
const chunks: { [file: string]: string[] } = {}

// APP-TODO: Remove this once we have CSS injections at each level.
const allChunks = new Set<string>()

for (const layoutOrPage in serverCSSManifest) {
const uniqueChunks = new Set<string>()
for (const css of serverCSSManifest[layoutOrPage]) {
for (const chunk of serverComponentManifest[css].default.chunks) {
if (!uniqueChunks.has(chunk)) {
uniqueChunks.add(chunk)
chunks[layoutOrPage] = chunks[layoutOrPage] || []
chunks[layoutOrPage].push(chunk)
}
allChunks.add(chunk)
}
for (const css of layoutOrPageCss) {
for (const chunk of serverComponentManifest[css].default.chunks) {
chunks.add(chunk)
}
}

return [...allChunks]
return [...chunks]
}

export async function renderToHTMLOrFlight(
Expand Down Expand Up @@ -618,29 +589,23 @@ export async function renderToHTMLOrFlight(
*/
const createComponentTree = async ({
createSegmentPath,
loaderTree: [
segment,
parallelRoutes,
{ /* filePath, */ layout, loading, page },
],
loaderTree: [segment, parallelRoutes, { filePath, layout, loading, page }],
parentParams,
firstItem,
rootLayoutIncluded,
}: // parentSegmentPath,
{
}: {
createSegmentPath: CreateSegmentPath
loaderTree: LoaderTree
parentParams: { [key: string]: any }
rootLayoutIncluded?: boolean
firstItem?: boolean
// parentSegmentPath: string
}): Promise<{ Component: React.ComponentType }> => {
// TODO-APP: enable stylesheet per layout/page
// const stylesheets = getCssInlinedLinkTags(
// serverComponentManifest,
// serverCSSManifest!,
// filePath
// )
const stylesheets = getCssInlinedLinkTags(
serverComponentManifest,
serverCSSManifest!,
filePath
)
const Loading = loading ? await interopDefault(loading()) : undefined
const isLayout = typeof layout !== 'undefined'
const isPage = typeof page !== 'undefined'
Expand Down Expand Up @@ -719,7 +684,6 @@ export async function renderToHTMLOrFlight(
loaderTree: parallelRoutes[parallelRouteKey],
parentParams: currentParams,
rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove,
// parentSegmentPath: cssSegmentPath,
})

const childSegment = parallelRoutes[parallelRouteKey][0]
Expand Down Expand Up @@ -860,11 +824,20 @@ export async function renderToHTMLOrFlight(

return (
<>
{/* {stylesheets
{stylesheets
? stylesheets.map((href) => (
<link rel="stylesheet" href={`/_next/${href}`} key={href} />
<link
rel="stylesheet"
href={`/_next/${href}?ts=${Date.now()}`}
// `Precedence` is an opt-in signal for React to handle
// resource loading and deduplication, etc:
// https://github.com/facebook/react/pull/25060
// @ts-ignore
precedence="high"
key={href}
/>
))
: null} */}
: null}
<Component
{...props}
{...parallelRouteComponents}
Expand Down Expand Up @@ -988,19 +961,12 @@ export async function renderToHTMLOrFlight(

// Below this line is handling for rendering to HTML.

// Get all the server imported styles.
const initialStylesheets = getAllCssInlinedLinkTags(
serverComponentManifest,
serverCSSManifest || {}
)

// Create full component tree from root to leaf.
const { Component: ComponentTree } = await createComponentTree({
createSegmentPath: (child) => child,
loaderTree: loaderTree,
parentParams: {},
firstItem: true,
// parentSegmentPath: '',
})

// AppRouter is provided by next-app-loader
Expand Down Expand Up @@ -1108,7 +1074,6 @@ export async function renderToHTMLOrFlight(
generateStaticHTML: generateStaticHTML,
flushEffectHandler,
flushEffectsToHead: true,
initialStylesheets,
})
}

Expand Down
7 changes: 1 addition & 6 deletions packages/next/server/node-web-streams-helper.ts
Expand Up @@ -265,14 +265,12 @@ export async function continueFromInitialStream(
generateStaticHTML,
flushEffectHandler,
flushEffectsToHead,
initialStylesheets,
}: {
suffix?: string
dataStream?: ReadableStream<Uint8Array>
generateStaticHTML: boolean
flushEffectHandler?: () => string
flushEffectsToHead: boolean
initialStylesheets?: string[]
}
): Promise<ReadableStream<Uint8Array>> {
const closeTag = '</body></html>'
Expand All @@ -291,14 +289,11 @@ export async function continueFromInitialStream(
dataStream ? createInlineDataStream(dataStream) : null,
suffixUnclosed != null ? createSuffixStream(closeTag) : null,
createHeadInjectionTransformStream(() => {
const inlineStyleLinks = (initialStylesheets || [])
.map((href) => `<link rel="stylesheet" href="/_next/${href}">`)
.join('')
// TODO-APP: Inject flush effects 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 flushEffectsContent =
flushEffectHandler && flushEffectsToHead ? flushEffectHandler() : ''
return inlineStyleLinks + flushEffectsContent
return flushEffectsContent
}),
].filter(nonNullable)

Expand Down
24 changes: 12 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/e2e/app-dir/app/app/css/css-page/style.css
@@ -1,3 +1,3 @@
h1 {
color: blueviolet;
color: red;
}
2 changes: 1 addition & 1 deletion test/e2e/app-dir/app/app/css/style.css
@@ -1,3 +1,3 @@
.server-css {
color: green;
color: blue;
}
3 changes: 3 additions & 0 deletions test/e2e/app-dir/app/app/style.css
@@ -0,0 +1,3 @@
body {
font-size: xx-large;
}