From c79b67ccedda1ae6fd9d05cfccf1d2842b94f43f Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 17 Aug 2022 12:56:52 +0200 Subject: [PATCH] Improved server CSS handling (#39664) Upgrade experimental React, and render link tags directly in the tree during development. The client bundle won't import CSS anymore, and server CSS imports will be transpiled into no-op strings just for HMR to use. ## Follow Ups - [ ] Flash of unstyled elements when reloading styles - [ ] Collect client style imports - [ ] Console warning for duplicated resources - [ ] Tests ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples) --- package.json | 4 +- .../build/webpack/config/blocks/css/index.ts | 35 ++++-- .../next-flight-client-entry-loader.ts | 6 +- .../loaders/next-flight-css-dev-loader.ts | 16 +++ packages/next/server/app-render.tsx | 101 ++++++------------ .../next/server/node-web-streams-helper.ts | 7 +- pnpm-lock.yaml | 24 ++--- .../app-dir/app/app/css/css-page/style.css | 2 +- test/e2e/app-dir/app/app/css/style.css | 2 +- test/e2e/app-dir/app/app/style.css | 3 + 10 files changed, 99 insertions(+), 101 deletions(-) create mode 100644 packages/next/build/webpack/loaders/next-flight-css-dev-loader.ts diff --git a/package.json b/package.json index 698760d043cf..fbc7d8c6f297 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/next/build/webpack/config/blocks/css/index.ts b/packages/next/build/webpack/config/blocks/css/index.ts index 1177e746a649..2585a2202262 100644 --- a/packages/next/build/webpack/config/blocks/css/index.ts +++ b/packages/next/build/webpack/config/blocks/css/index.ts @@ -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({ diff --git a/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts b/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts index e30d5037569d..f168350122d8 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts @@ -21,7 +21,11 @@ export default async function transformSource(this: any): Promise { 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__ = { diff --git a/packages/next/build/webpack/loaders/next-flight-css-dev-loader.ts b/packages/next/build/webpack/loaders/next-flight-css-dev-loader.ts new file mode 100644 index 000000000000..d2f9ab521466 --- /dev/null +++ b/packages/next/build/webpack/loaders/next-flight-css-dev-loader.ts @@ -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 diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 6ae16e35cc0b..2c221e3e75e7 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -378,55 +378,26 @@ function getSegmentParam(segment: string): { /** * Get inline 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() +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() -/** - * Get inline 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() - - for (const layoutOrPage in serverCSSManifest) { - const uniqueChunks = new Set() - 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( @@ -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' @@ -719,7 +684,6 @@ export async function renderToHTMLOrFlight( loaderTree: parallelRoutes[parallelRouteKey], parentParams: currentParams, rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove, - // parentSegmentPath: cssSegmentPath, }) const childSegment = parallelRoutes[parallelRouteKey][0] @@ -860,11 +824,20 @@ export async function renderToHTMLOrFlight( return ( <> - {/* {stylesheets + {stylesheets ? stylesheets.map((href) => ( - + )) - : null} */} + : null} child, loaderTree: loaderTree, parentParams: {}, firstItem: true, - // parentSegmentPath: '', }) // AppRouter is provided by next-app-loader @@ -1108,7 +1074,6 @@ export async function renderToHTMLOrFlight( generateStaticHTML: generateStaticHTML, flushEffectHandler, flushEffectsToHead: true, - initialStylesheets, }) } diff --git a/packages/next/server/node-web-streams-helper.ts b/packages/next/server/node-web-streams-helper.ts index 65044c328e2a..2ffaaf5bf131 100644 --- a/packages/next/server/node-web-streams-helper.ts +++ b/packages/next/server/node-web-streams-helper.ts @@ -265,14 +265,12 @@ export async function continueFromInitialStream( generateStaticHTML, flushEffectHandler, flushEffectsToHead, - initialStylesheets, }: { suffix?: string dataStream?: ReadableStream generateStaticHTML: boolean flushEffectHandler?: () => string flushEffectsToHead: boolean - initialStylesheets?: string[] } ): Promise> { const closeTag = '' @@ -291,14 +289,11 @@ export async function continueFromInitialStream( dataStream ? createInlineDataStream(dataStream) : null, suffixUnclosed != null ? createSuffixStream(closeTag) : null, createHeadInjectionTransformStream(() => { - const inlineStyleLinks = (initialStylesheets || []) - .map((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) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70af320d27c8..d23d5008f024 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -140,8 +140,8 @@ importers: 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 @@ -294,8 +294,8 @@ importers: react-17: /react/17.0.2 react-dom: 18.2.0_react@18.2.0 react-dom-17: /react-dom/17.0.2_react@18.2.0 - react-dom-exp: /react-dom/0.0.0-experimental-4cd788aef-20220630_react@18.2.0 - react-exp: /react/0.0.0-experimental-4cd788aef-20220630 + react-dom-exp: /react-dom/0.0.0-experimental-6ef466c68-20220816_react@18.2.0 + react-exp: /react/0.0.0-experimental-6ef466c68-20220816 react-ssr-prepass: 1.0.8_qncsgtzehe3fgiqp6tr7lwq6fm react-virtualized: 9.22.3_biqbaboplfbrettd7655fr4n2y relay-compiler: 13.0.2 @@ -18138,14 +18138,14 @@ packages: strip-json-comments: 2.0.1 dev: true - /react-dom/0.0.0-experimental-4cd788aef-20220630_react@18.2.0: - resolution: {integrity: sha512-VN80AwDQ19WbcX70m/3/ho/eahBIMVsrxbP7C0KU/tNe6BRwc7+QzvdDxSb2CwLlk/MZrOansFpQYEefn90w9Q==} + /react-dom/0.0.0-experimental-6ef466c68-20220816_react@18.2.0: + resolution: {integrity: sha512-OSHyYEL5s/Wbxe0ksihDOGLKUbmqT+FeYmZ8U6upTg+V6VIDLcgolzBWbP/QXwvjR99Qc5vhb2kHHXb2zYOnJw==} peerDependencies: - react: 0.0.0-experimental-4cd788aef-20220630 + react: 0.0.0-experimental-6ef466c68-20220816 dependencies: loose-envify: 1.4.0 react: 18.2.0 - scheduler: 0.0.0-experimental-4cd788aef-20220630 + scheduler: 0.0.0-experimental-6ef466c68-20220816 dev: true /react-dom/17.0.2_react@18.2.0: @@ -18229,8 +18229,8 @@ packages: react-lifecycles-compat: 3.0.4 dev: true - /react/0.0.0-experimental-4cd788aef-20220630: - resolution: {integrity: sha512-mPez9MeF8whDoKAtz7bGvwOWn3THEzZvftQAGAlzrohcszPDOyq1GEc3sFLKDZTSgRzVzALXcfaVoJ0EqWcgnw==} + /react/0.0.0-experimental-6ef466c68-20220816: + resolution: {integrity: sha512-UixkYaiwN2Ep7tqRATjwc7sFsk4cEi6t7KLAZ7MwlZI/3+ZGPk5f1J7/YvLU7STncEY2NC6GDyVgNmrK8Ov77g==} engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 @@ -19226,8 +19226,8 @@ packages: xmlchars: 2.2.0 dev: true - /scheduler/0.0.0-experimental-4cd788aef-20220630: - resolution: {integrity: sha512-ywbmX5l/JpTWQbUy9IcHO0fzaHGL/IIgM5lj1IsJA0KYn3IZx6fJkXiVRFFY1GuXAsLsalniC+SM5NLjUED8fg==} + /scheduler/0.0.0-experimental-6ef466c68-20220816: + resolution: {integrity: sha512-3d6wgYQA2rFc62tqtEPJdnd0rxfq8VTk8Toj3v7LEdZ4vB3pACNxthU3E3WPDtXbHAUQ+liMgFwuXxeE+8aDhg==} dependencies: loose-envify: 1.4.0 dev: true diff --git a/test/e2e/app-dir/app/app/css/css-page/style.css b/test/e2e/app-dir/app/app/css/css-page/style.css index 61083abf180c..adc68fa6a4df 100644 --- a/test/e2e/app-dir/app/app/css/css-page/style.css +++ b/test/e2e/app-dir/app/app/css/css-page/style.css @@ -1,3 +1,3 @@ h1 { - color: blueviolet; + color: red; } diff --git a/test/e2e/app-dir/app/app/css/style.css b/test/e2e/app-dir/app/app/css/style.css index efd81fda0528..26994cdddaee 100644 --- a/test/e2e/app-dir/app/app/css/style.css +++ b/test/e2e/app-dir/app/app/css/style.css @@ -1,3 +1,3 @@ .server-css { - color: green; + color: blue; } diff --git a/test/e2e/app-dir/app/app/style.css b/test/e2e/app-dir/app/app/style.css index e69de29bb2d1..0b8fbd008481 100644 --- a/test/e2e/app-dir/app/app/style.css +++ b/test/e2e/app-dir/app/app/style.css @@ -0,0 +1,3 @@ +body { + font-size: xx-large; +}