11import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin' ;
22import browserslist from 'browserslist' ;
33import { 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' ;
58import 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
716function getSupportedBrowsers ( dir : any , isDevelopment : any ) {
817 let browsers ;
@@ -18,12 +27,82 @@ function getSupportedBrowsers(dir: any, isDevelopment: any) {
1827
1928type 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+
2182export 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 : / \. v a n i l l a \. c s s $ / 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