Skip to content

Commit

Permalink
HMR for client CSS imports (#39916)
Browse files Browse the repository at this point in the history
Follow-up to #39758, this PR makes sure that CSS imports (both global and CSS modules) from client components are not handled by mini-css-extract's HMR logic. Instead, we trigger a server component update and let the client to refetch the RSC payload.

However, we are still leveraging the mini-css-extract plugin to emit CSS assets. So in this PR we add a new pitch loader to calculate the original file hash, but replace the final content to eliminate HMR logic but only keep the hash (so hot reloader can keep tracking that).

## 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)
  • Loading branch information
shuding committed Aug 25, 2022
1 parent a799d25 commit f12788d
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 149 deletions.
305 changes: 180 additions & 125 deletions packages/next/build/webpack/config/blocks/css/index.ts
Expand Up @@ -201,59 +201,121 @@ export const css = curry(async function css(

// CSS Modules support must be enabled on the server and client so the class
// names are available for SSR or Prerendering.
fns.push(
loader({
oneOf: [
markRemovable({
// CSS Modules should never have side effects. This setting will
// allow unused CSS to be removed from the production build.
// We ensure this by disallowing `:global()` CSS at the top-level
// via the `pure` mode in `css-loader`.
sideEffects: false,
// CSS Modules are activated via this specific extension.
test: regexCssModules,
// CSS Modules are only supported in the user's application. We're
// not yet allowing CSS imports _within_ `node_modules`.
issuer: {
and: [
{
or: [ctx.rootDirectory, regexClientEntry],
},
if (ctx.experimental.appDir && !ctx.isProduction) {
fns.push(
loader({
oneOf: [
markRemovable({
// CSS Modules should never have side effects. This setting will
// allow unused CSS to be removed from the production build.
// We ensure this by disallowing `:global()` CSS at the top-level
// via the `pure` mode in `css-loader`.
sideEffects: false,
// CSS Modules are activated via this specific extension.
test: regexCssModules,
// CSS Modules are only supported in the user's application. We're
// not yet allowing CSS imports _within_ `node_modules`.
issuer: {
and: [
{
or: [ctx.rootDirectory, regexClientEntry],
},
],
not: [/node_modules/],
},
use: [
require.resolve('../../../loaders/next-flight-css-dev-loader'),
...getCssModuleLoader(ctx, lazyPostCSSInitializer),
],
not: [/node_modules/],
},
use: getCssModuleLoader(ctx, lazyPostCSSInitializer),
}),
],
})
)
fns.push(
loader({
oneOf: [
// Opt-in support for Sass (using .scss or .sass extensions).
markRemovable({
// Sass Modules should never have side effects. This setting will
// allow unused Sass to be removed from the production build.
// We ensure this by disallowing `:global()` Sass at the top-level
// via the `pure` mode in `css-loader`.
sideEffects: false,
// Sass Modules are activated via this specific extension.
test: regexSassModules,
// Sass Modules are only supported in the user's application. We're
// not yet allowing Sass imports _within_ `node_modules`.
issuer: {
and: [ctx.rootDirectory],
not: [/node_modules/],
},
use: getCssModuleLoader(
ctx,
lazyPostCSSInitializer,
sassPreprocessors
),
}),
],
})
)
}),
],
})
)
fns.push(
loader({
oneOf: [
// Opt-in support for Sass (using .scss or .sass extensions).
markRemovable({
// Sass Modules should never have side effects. This setting will
// allow unused Sass to be removed from the production build.
// We ensure this by disallowing `:global()` Sass at the top-level
// via the `pure` mode in `css-loader`.
sideEffects: false,
// Sass Modules are activated via this specific extension.
test: regexSassModules,
// Sass Modules are only supported in the user's application. We're
// not yet allowing Sass imports _within_ `node_modules`.
issuer: {
and: [ctx.rootDirectory],
not: [/node_modules/],
},
use: [
require.resolve('../../../loaders/next-flight-css-dev-loader'),
...getCssModuleLoader(
ctx,
lazyPostCSSInitializer,
sassPreprocessors
),
],
}),
],
})
)
} else {
fns.push(
loader({
oneOf: [
markRemovable({
// CSS Modules should never have side effects. This setting will
// allow unused CSS to be removed from the production build.
// We ensure this by disallowing `:global()` CSS at the top-level
// via the `pure` mode in `css-loader`.
sideEffects: false,
// CSS Modules are activated via this specific extension.
test: regexCssModules,
// CSS Modules are only supported in the user's application. We're
// not yet allowing CSS imports _within_ `node_modules`.
issuer: {
and: [
{
or: [ctx.rootDirectory, regexClientEntry],
},
],
not: [/node_modules/],
},
use: getCssModuleLoader(ctx, lazyPostCSSInitializer),
}),
],
})
)
fns.push(
loader({
oneOf: [
// Opt-in support for Sass (using .scss or .sass extensions).
markRemovable({
// Sass Modules should never have side effects. This setting will
// allow unused Sass to be removed from the production build.
// We ensure this by disallowing `:global()` Sass at the top-level
// via the `pure` mode in `css-loader`.
sideEffects: false,
// Sass Modules are activated via this specific extension.
test: regexSassModules,
// Sass Modules are only supported in the user's application. We're
// not yet allowing Sass imports _within_ `node_modules`.
issuer: {
and: [ctx.rootDirectory],
not: [/node_modules/],
},
use: getCssModuleLoader(
ctx,
lazyPostCSSInitializer,
sassPreprocessors
),
}),
],
})
)
}

if (!ctx.experimental.appDir) {
// Throw an error for CSS Modules used outside their supported scope
Expand All @@ -280,6 +342,7 @@ export const css = curry(async function css(
loader({
oneOf: [
markRemovable({
sideEffects: true,
test: [regexCssGlobal, regexSassGlobal],
use: require.resolve(
'../../../loaders/next-flight-css-dev-loader'
Expand All @@ -301,38 +364,7 @@ export const css = curry(async function css(
)
}
} else {
fns.push(
loader({
oneOf: [
markRemovable({
// A global CSS import always has side effects. Webpack will tree
// shake the CSS without this option if the issuer claims to have
// no side-effects.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
test: regexCssGlobal,
// We only allow Global CSS to be imported anywhere in the
// application if it comes from node_modules. This is a best-effort
// heuristic that makes a safety trade-off for better
// interoperability with npm packages that require CSS. Without
// this ability, the component's CSS would have to be included for
// the entire app instead of specific page where it's required.
include: { and: [/node_modules/] },
// Global CSS is only supported in the user's application, not in
// node_modules.
issuer: ctx.experimental.craCompat
? undefined
: {
and: [ctx.rootDirectory],
not: [/node_modules/],
},
use: getGlobalCssLoader(ctx, lazyPostCSSInitializer),
}),
],
})
)

if (ctx.customAppFile) {
if (ctx.experimental.appDir) {
fns.push(
loader({
oneOf: [
Expand All @@ -343,8 +375,10 @@ export const css = curry(async function css(
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
test: regexCssGlobal,
issuer: { and: [ctx.customAppFile] },
use: getGlobalCssLoader(ctx, lazyPostCSSInitializer),
use: [
require.resolve('../../../loaders/next-flight-css-dev-loader'),
...getGlobalCssLoader(ctx, lazyPostCSSInitializer),
],
}),
],
})
Expand All @@ -353,25 +387,17 @@ export const css = curry(async function css(
loader({
oneOf: [
markRemovable({
// A global Sass import always has side effects. Webpack will tree
// shake the Sass without this option if the issuer claims to have
// no side-effects.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
test: regexSassGlobal,
issuer: { and: [ctx.customAppFile] },
use: getGlobalCssLoader(
ctx,
lazyPostCSSInitializer,
sassPreprocessors
),
sideEffects: false,
test: regexCssModules,
use: [
require.resolve('../../../loaders/next-flight-css-dev-loader'),
...getCssModuleLoader(ctx, lazyPostCSSInitializer),
],
}),
],
})
)
}

if (ctx.experimental.appDir) {
} else {
fns.push(
loader({
oneOf: [
Expand All @@ -382,36 +408,65 @@ export const css = curry(async function css(
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
test: regexCssGlobal,
issuer: {
and: [
{
or: [
{ and: [ctx.rootDirectory, /\.(js|mjs|jsx|ts|tsx)$/] },
regexClientEntry,
],
// We only allow Global CSS to be imported anywhere in the
// application if it comes from node_modules. This is a best-effort
// heuristic that makes a safety trade-off for better
// interoperability with npm packages that require CSS. Without
// this ability, the component's CSS would have to be included for
// the entire app instead of specific page where it's required.
include: { and: [/node_modules/] },
// Global CSS is only supported in the user's application, not in
// node_modules.
issuer: ctx.experimental.craCompat
? undefined
: {
and: [ctx.rootDirectory],
not: [/node_modules/],
},
],
},
use: getGlobalCssLoader(ctx, lazyPostCSSInitializer),
}),
],
})
)
fns.push(
loader({
oneOf: [
markRemovable({
sideEffects: false,
test: regexCssModules,
issuer: {
and: [ctx.rootDirectory, /\.(js|mjs|jsx|ts|tsx)$/],
or: [regexClientEntry],
},
use: getCssModuleLoader(ctx, lazyPostCSSInitializer),
}),
],
})
)

if (ctx.customAppFile) {
fns.push(
loader({
oneOf: [
markRemovable({
// A global CSS import always has side effects. Webpack will tree
// shake the CSS without this option if the issuer claims to have
// no side-effects.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
test: regexCssGlobal,
issuer: { and: [ctx.customAppFile] },
use: getGlobalCssLoader(ctx, lazyPostCSSInitializer),
}),
],
})
)
fns.push(
loader({
oneOf: [
markRemovable({
// A global Sass import always has side effects. Webpack will tree
// shake the Sass without this option if the issuer claims to have
// no side-effects.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
test: regexSassGlobal,
issuer: { and: [ctx.customAppFile] },
use: getGlobalCssLoader(
ctx,
lazyPostCSSInitializer,
sassPreprocessors
),
}),
],
})
)
}
}
}

Expand Down
Expand Up @@ -40,8 +40,10 @@ export function getClientStyleLoader({
const MiniCssExtractPlugin =
require('../../../../plugins/mini-css-extract-plugin').default
return {
// @ts-ignore: TODO: remove when webpack 5 is stable
loader: MiniCssExtractPlugin.loader,
options: { publicPath: `${assetPrefix}/_next/`, esModule: false },
options: {
publicPath: `${assetPrefix}/_next/`,
esModule: false,
},
}
}

0 comments on commit f12788d

Please sign in to comment.