1
1
import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin' ;
2
2
import browserslist from 'browserslist' ;
3
3
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' ;
5
8
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 ;
6
15
7
16
function getSupportedBrowsers ( dir : any , isDevelopment : any ) {
8
17
let browsers ;
@@ -18,12 +27,82 @@ function getSupportedBrowsers(dir: any, isDevelopment: any) {
18
27
19
28
type PluginOptions = ConstructorParameters < typeof VanillaExtractPlugin > [ 0 ] ;
20
29
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
+
21
82
export const createVanillaExtractPlugin =
22
83
( pluginOptions : PluginOptions = { } ) =>
23
84
( nextConfig : NextConfig = { } ) : NextConfig =>
24
85
Object . assign ( { } , nextConfig , {
25
- webpack ( config : any , options : any ) {
86
+ webpack ( config : any , options : WebpackConfigContext ) {
26
87
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 ;
27
106
28
107
const cssRules = config . module . rules . find (
29
108
( rule : any ) =>
@@ -39,24 +118,54 @@ export const createVanillaExtractPlugin =
39
118
cssRules . unshift ( {
40
119
test : / \. v a n i l l a \. c s s $ / i,
41
120
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 ,
55
124
) ,
56
125
} ) ;
57
126
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
+
58
167
config . plugins . push (
59
- new VanillaExtractPlugin ( { outputCss : ! isServer , ...pluginOptions } ) ,
168
+ new VanillaExtractPlugin ( { outputCss, ...pluginOptions } ) ,
60
169
) ;
61
170
62
171
if ( typeof nextConfig . webpack === 'function' ) {
0 commit comments