Skip to content

Commit 9541d79

Browse files
authored
fix(nextjs-plugin): properly handles css loader (#1105)
1 parent 0228483 commit 9541d79

File tree

5 files changed

+261
-1008
lines changed

5 files changed

+261
-1008
lines changed

.changeset/witty-ties-bow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@vanilla-extract/next-plugin': minor
3+
---
4+
5+
Fix #1101. Correctly handle Next.js configuration.

examples/next/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"start": "next start"
1010
},
1111
"dependencies": {
12-
"next": "^12.0.5",
12+
"next": "12.3.4",
1313
"react": "^17.0.2",
1414
"react-dom": "^17.0.2"
1515
},

packages/next-plugin/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
"browserslist": "^4.19.1"
2020
},
2121
"peerDependencies": {
22-
"next": ">=12.0.5"
22+
"next": ">=12.1.7"
2323
},
2424
"devDependencies": {
25-
"next": "^12.0.5"
25+
"next": "12.3.4",
26+
"webpack": "^5.36.1"
2627
}
2728
}

packages/next-plugin/src/index.ts

Lines changed: 125 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin';
22
import browserslist from 'browserslist';
33
import { lazyPostCSS } from 'next/dist/build/webpack/config/blocks/css';
4-
import { getGlobalCssLoader } from 'next/dist/build/webpack/config/blocks/css/loaders';
4+
import { findPagesDir } from 'next/dist/lib/find-pages-dir';
5+
import nextMiniCssExtractPluginExports from 'next/dist/build/webpack/plugins/mini-css-extract-plugin';
6+
7+
import type webpack from 'webpack';
58
import type { NextConfig } from 'next/types';
9+
import type { WebpackConfigContext } from 'next/dist/server/config-shared';
10+
11+
// Next.js' built-in mini-css-extract-plugin has a very terrible type definition, let's just use any
12+
const NextMiniCssExtractPlugin: any =
13+
// @ts-expect-error -- Next.js' precompilation does add "__esModule: true", but doesn't add an actual default exports
14+
nextMiniCssExtractPluginExports.default;
615

716
function getSupportedBrowsers(dir: any, isDevelopment: any) {
817
let browsers;
@@ -18,12 +27,82 @@ function getSupportedBrowsers(dir: any, isDevelopment: any) {
1827

1928
type PluginOptions = ConstructorParameters<typeof VanillaExtractPlugin>[0];
2029

30+
// https://github.com/vercel/next.js/blob/canary/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L7
31+
const getVanillaExtractCssLoaders = (
32+
options: WebpackConfigContext,
33+
assetPrefix: string,
34+
) => {
35+
const loaders: webpack.RuleSetUseItem[] = [];
36+
37+
// https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L14
38+
if (!options.isServer) {
39+
// https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/client.ts#L44
40+
// next-style-loader will mess up css order in development mode.
41+
// Next.js appDir doesn't use next-style-loader either.
42+
// So we always use css-loader here, to simplify things and get proper order of output CSS
43+
loaders.push({
44+
loader: NextMiniCssExtractPlugin.loader,
45+
options: {
46+
publicPath: `${assetPrefix}/_next/`,
47+
esModule: false,
48+
},
49+
});
50+
}
51+
52+
const postcss = () =>
53+
lazyPostCSS(
54+
options.dir,
55+
getSupportedBrowsers(options.dir, options.dev),
56+
undefined,
57+
);
58+
59+
// https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L28
60+
loaders.push({
61+
loader: require.resolve('next/dist/build/webpack/loaders/css-loader/src'),
62+
options: {
63+
postcss,
64+
importLoaders: 1,
65+
modules: false,
66+
},
67+
});
68+
69+
// https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L43
70+
loaders.push({
71+
loader: require.resolve(
72+
'next/dist/build/webpack/loaders/postcss-loader/src',
73+
),
74+
options: {
75+
postcss,
76+
},
77+
});
78+
79+
return loaders;
80+
};
81+
2182
export const createVanillaExtractPlugin =
2283
(pluginOptions: PluginOptions = {}) =>
2384
(nextConfig: NextConfig = {}): NextConfig =>
2485
Object.assign({}, nextConfig, {
25-
webpack(config: any, options: any) {
86+
webpack(config: any, options: WebpackConfigContext) {
2687
const { dir, dev, isServer, config: resolvedNextConfig } = options;
88+
const findPagesDirResult = findPagesDir(
89+
dir,
90+
resolvedNextConfig.experimental?.appDir,
91+
);
92+
93+
// https://github.com/vercel/next.js/blob/1fb4cad2a8329811b5ccde47217b4a6ae739124e/packages/next/build/index.ts#L336
94+
// https://github.com/vercel/next.js/blob/1fb4cad2a8329811b5ccde47217b4a6ae739124e/packages/next/build/webpack-config.ts#L626
95+
// https://github.com/vercel/next.js/pull/43916
96+
const hasAppDir =
97+
// on Next.js 12, findPagesDirResult is a string. on Next.js 13, findPagesDirResult is an object
98+
!!resolvedNextConfig.experimental?.appDir &&
99+
!!(findPagesDirResult && findPagesDirResult.appDir);
100+
101+
const outputCss = hasAppDir
102+
? // Always output css since Next.js App Router needs to collect Server CSS from React Server Components
103+
true
104+
: // There is no appDir, do not output css on server build
105+
!isServer;
27106

28107
const cssRules = config.module.rules.find(
29108
(rule: any) =>
@@ -39,24 +118,54 @@ export const createVanillaExtractPlugin =
39118
cssRules.unshift({
40119
test: /\.vanilla\.css$/i,
41120
sideEffects: true,
42-
use: getGlobalCssLoader(
43-
{
44-
assetPrefix: config.assetPrefix,
45-
isClient: !isServer,
46-
isServer,
47-
isDevelopment: dev,
48-
future: resolvedNextConfig.future || {},
49-
experimental: resolvedNextConfig.experimental || {},
50-
// @ts-ignore -- 'appDir' config is in beta
51-
hasAppDir: resolvedNextConfig.experimental?.appDir,
52-
} as any,
53-
() => lazyPostCSS(dir, getSupportedBrowsers(dir, dev), undefined),
54-
[],
121+
use: getVanillaExtractCssLoaders(
122+
options,
123+
resolvedNextConfig.assetPrefix,
55124
),
56125
});
57126

127+
// vanilla-extract need to emit the css file on both server and client, both during the
128+
// development and production.
129+
// However, Next.js only add MiniCssExtractPlugin on pages dir + client build + production mode.
130+
//
131+
// To simplify the logic at our side, we will add MiniCssExtractPlugin based on
132+
// the "instanceof" check (We will only add our required MiniCssExtractPlugin if
133+
// Next.js hasn't added it yet).
134+
// This also prevent multiple MiniCssExtractPlugin being added (which will cause
135+
// RealContentHashPlugin to panic)
136+
if (
137+
!config.plugins.some(
138+
(plugin: any) => plugin instanceof NextMiniCssExtractPlugin,
139+
)
140+
) {
141+
// HMR reloads the CSS file when the content changes but does not use
142+
// the new file name, which means it can't contain a hash.
143+
const filename = dev
144+
? 'static/css/[name].css'
145+
: 'static/css/[contenthash].css';
146+
147+
config.plugins.push(
148+
new NextMiniCssExtractPlugin({
149+
filename,
150+
chunkFilename: filename,
151+
// Next.js guarantees that CSS order "doesn't matter", due to imposed
152+
// restrictions:
153+
// 1. Global CSS can only be defined in a single entrypoint (_app)
154+
// 2. CSS Modules generate scoped class names by default and cannot
155+
// include Global CSS (:global() selector).
156+
//
157+
// While not a perfect guarantee (e.g. liberal use of `:global()`
158+
// selector), this assumption is required to code-split CSS.
159+
//
160+
// If this warning were to trigger, it'd be unactionable by the user,
161+
// but likely not valid -- so just disable it.
162+
ignoreOrder: true,
163+
}),
164+
);
165+
}
166+
58167
config.plugins.push(
59-
new VanillaExtractPlugin({ outputCss: !isServer, ...pluginOptions }),
168+
new VanillaExtractPlugin({ outputCss, ...pluginOptions }),
60169
);
61170

62171
if (typeof nextConfig.webpack === 'function') {

0 commit comments

Comments
 (0)