Skip to content

Commit

Permalink
fix(next-plugin): build not outputting css on windows (#1180)
Browse files Browse the repository at this point in the history
* fix(next-plugin): build not outputting css on windows

* Create wise-oranges-talk.md

* Format code

---------

Co-authored-by: Adam Skoufis <[email protected]>
Co-authored-by: Adam Skoufis <[email protected]>
  • Loading branch information
3 people authored Sep 16, 2023
1 parent 58005eb commit 89224fe
Show file tree
Hide file tree
Showing 14 changed files with 363 additions and 272 deletions.
6 changes: 6 additions & 0 deletions .changeset/wise-oranges-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@vanilla-extract/next-plugin": patch
"@vanilla-extract/webpack-plugin": patch
---

Fixes Next.js 13 CSS output on Windows when using React Server Components
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ test-results

.pnp.*
.next
.DS_Store
.DS_Store
.idea
8 changes: 6 additions & 2 deletions packages/next-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"description": "Zero-runtime Stylesheets-in-TypeScript",
"main": "dist/vanilla-extract-next-plugin.cjs.js",
"module": "dist/vanilla-extract-next-plugin.esm.js",
"preconstruct": {
"entrypoints": [
"index.ts"
]
},
"files": [
"/dist"
],
Expand All @@ -15,8 +20,7 @@
"author": "SEEK",
"license": "MIT",
"dependencies": {
"@vanilla-extract/webpack-plugin": "^2.3.0",
"browserslist": "^4.19.1"
"@vanilla-extract/webpack-plugin": "^2.3.0"
},
"peerDependencies": {
"next": ">=12.1.7"
Expand Down
230 changes: 121 additions & 109 deletions packages/next-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,45 @@
import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin';
import browserslist from 'browserslist';
import { lazyPostCSS } from 'next/dist/build/webpack/config/blocks/css';
// @ts-expect-error
import browserslist from 'next/dist/compiled/browserslist';
import NextMiniCssExtractPluginDefault from 'next/dist/build/webpack/plugins/mini-css-extract-plugin';
import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin/next';
import { findPagesDir } from 'next/dist/lib/find-pages-dir';
import { lazyPostCSS } from 'next/dist/build/webpack/config/blocks/css';
import { cssFileResolve } from 'next/dist/build/webpack/config/blocks/css/loaders/file-resolve';
import NextMiniCssExtractPluginDefault from 'next/dist/build/webpack/plugins/mini-css-extract-plugin';

import type webpack from 'webpack';
import type { NextConfig } from 'next/types';
import type { WebpackConfigContext } from 'next/dist/server/config-shared';
import type {
NextConfig,
WebpackConfigContext,
} from 'next/dist/server/config-shared';

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

const NextMiniCssExtractPlugin = NextMiniCssExtractPluginDefault as any;

function getSupportedBrowsers(dir: any, isDevelopment: any) {
let browsers;
// Adopted from https://github.com/vercel/next.js/blob/1f1632979c78b3edfe59fd85d8cce62efcdee688/packages/next/build/webpack-config.ts#L60-L72
function getSupportedBrowsers(dir: string, isDevelopment: boolean) {
try {
browsers = browserslist.loadConfig({
return browserslist.loadConfig({
path: dir,
env: isDevelopment ? 'development' : 'production',
});
} catch {}

return browsers;
} catch (_) {
return undefined;
}
}

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

// https://github.com/vercel/next.js/blob/canary/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L7
// Adopt from Next.js' getGlobalCssLoader
// https://github.com/vercel/next.js/blob/6e5b935fd7a61497f6854a81aec7df3a5dbf61ac/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L7
const getVanillaExtractCssLoaders = (
options: WebpackConfigContext,
assetPrefix: string,
) => {
const loaders: webpack.RuleSetUseItem[] = [];

// https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L14
// Adopt from Next.js' getClientStyleLoader
// https://github.com/vercel/next.js/blob/6e5b935fd7a61497f6854a81aec7df3a5dbf61ac/packages/next/src/build/webpack/config/blocks/css/loaders/client.ts#L3
if (!options.isServer) {
// https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/client.ts#L44
// https://github.com/vercel/next.js/blob/6e5b935fd7a61497f6854a81aec7df3a5dbf61ac/packages/next/src/build/webpack/config/blocks/css/loaders/client.ts#L44
// next-style-loader will mess up css order in development mode.
// Next.js appDir doesn't use next-style-loader either.
// So we always use css-loader here, to simplify things and get proper order of output CSS
Expand All @@ -54,7 +59,7 @@ const getVanillaExtractCssLoaders = (
undefined,
);

// https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L28
// https://github.com/vercel/next.js/blob/6e5b935fd7a61497f6854a81aec7df3a5dbf61ac/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L28
loaders.push({
loader: require.resolve('next/dist/build/webpack/loaders/css-loader/src'),
options: {
Expand All @@ -76,7 +81,7 @@ const getVanillaExtractCssLoaders = (
},
});

// https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L43
// https://github.com/vercel/next.js/blob/6e5b935fd7a61497f6854a81aec7df3a5dbf61ac/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L29-L38
loaders.push({
loader: require.resolve(
'next/dist/build/webpack/loaders/postcss-loader/src',
Expand All @@ -86,102 +91,109 @@ const getVanillaExtractCssLoaders = (
},
});

// https://github.com/SukkaW/style9-webpack/blob/f51c46bbcd95ea3b988d3559c3b35cc056874366/src/next-appdir/index.ts#L103-L105
loaders.push({
loader: VanillaExtractPlugin.loader,
});

return loaders;
};

export const createVanillaExtractPlugin =
(pluginOptions: PluginOptions = {}) =>
(nextConfig: NextConfig = {}): NextConfig =>
Object.assign({}, nextConfig, {
webpack(config: any, options: WebpackConfigContext) {
const { dir, dev, isServer, config: resolvedNextConfig } = options;
const findPagesDirResult = findPagesDir(
dir,
resolvedNextConfig.experimental?.appDir,
);

// https://github.com/vercel/next.js/blob/1fb4cad2a8329811b5ccde47217b4a6ae739124e/packages/next/build/index.ts#L336
// https://github.com/vercel/next.js/blob/1fb4cad2a8329811b5ccde47217b4a6ae739124e/packages/next/build/webpack-config.ts#L626
// https://github.com/vercel/next.js/pull/43916
const hasAppDir =
// on Next.js 12, findPagesDirResult is a string. on Next.js 13, findPagesDirResult is an object
!!resolvedNextConfig.experimental?.appDir &&
!!(findPagesDirResult && findPagesDirResult.appDir);

const outputCss = hasAppDir
? // Always output css since Next.js App Router needs to collect Server CSS from React Server Components
true
: // There is no appDir, do not output css on server build
!isServer;

const cssRules = config.module.rules.find(
(rule: any) =>
Array.isArray(rule.oneOf) &&
rule.oneOf.some(
({ test }: any) =>
typeof test === 'object' &&
typeof test.test === 'function' &&
test.test('filename.css'),
),
).oneOf;

cssRules.unshift({
test: /\.vanilla\.css$/i,
sideEffects: true,
use: getVanillaExtractCssLoaders(
options,
resolvedNextConfig.assetPrefix,
export const createVanillaExtractPlugin = (
pluginOptions: PluginOptions = {},
) => {
return (nextConfig: NextConfig = {}): NextConfig => ({
...nextConfig,
webpack(config: any, options: WebpackConfigContext) {
const { dir, dev, isServer, config: resolvedNextConfig } = options;

// https://github.com/vercel/next.js/blob/1fb4cad2a8329811b5ccde47217b4a6ae739124e/packages/next/build/index.ts#L336
// https://github.com/vercel/next.js/blob/1fb4cad2a8329811b5ccde47217b4a6ae739124e/packages/next/build/webpack-config.ts#L626
// https://github.com/vercel/next.js/pull/43916
// on Next.js 12, findPagesDirResult is a string. on Next.js 13, findPagesDirResult is an object
const findPagesDirResult = findPagesDir(
dir,
resolvedNextConfig.experimental?.appDir ?? false,
);
const hasAppDir =
!!resolvedNextConfig.experimental?.appDir &&
!!(findPagesDirResult && findPagesDirResult.appDir);

const outputCss = hasAppDir
? // Always output css since Next.js App Router needs to collect Server CSS from React Server Components
true
: // There is no appDir, do not output css on server build
!isServer;

// https://github.com/vercel/next.js/blob/6e5b935fd7a61497f6854a81aec7df3a5dbf61ac/packages/next/src/build/webpack/config/helpers.ts#L12-L21
const cssRules = config.module.rules.find(
(rule: any) =>
Array.isArray(rule.oneOf) &&
rule.oneOf.some(
({ test }: any) =>
typeof test === 'object' &&
typeof test.test === 'function' &&
test.test('filename.css'),
),
});

// vanilla-extract need to emit the css file on both server and client, both during the
// development and production.
// However, Next.js only add MiniCssExtractPlugin on pages dir + client build + production mode.
//
// To simplify the logic at our side, we will add MiniCssExtractPlugin based on
// the "instanceof" check (We will only add our required MiniCssExtractPlugin if
// Next.js hasn't added it yet).
// This also prevent multiple MiniCssExtractPlugin being added (which will cause
// RealContentHashPlugin to panic)
if (
!config.plugins.some(
(plugin: any) => plugin instanceof NextMiniCssExtractPlugin,
)
) {
// HMR reloads the CSS file when the content changes but does not use
// the new file name, which means it can't contain a hash.
const filename = dev
? 'static/css/[name].css'
: 'static/css/[contenthash].css';

config.plugins.push(
new NextMiniCssExtractPlugin({
filename,
chunkFilename: filename,
// Next.js guarantees that CSS order "doesn't matter", due to imposed
// restrictions:
// 1. Global CSS can only be defined in a single entrypoint (_app)
// 2. CSS Modules generate scoped class names by default and cannot
// include Global CSS (:global() selector).
//
// While not a perfect guarantee (e.g. liberal use of `:global()`
// selector), this assumption is required to code-split CSS.
//
// If this warning were to trigger, it'd be unactionable by the user,
// but likely not valid -- so just disable it.
ignoreOrder: true,
}),
);
}
).oneOf;

// https://github.com/SukkaW/style9-webpack/blob/f51c46bbcd95ea3b988d3559c3b35cc056874366/src/next-appdir/index.ts#L187-L190
cssRules.unshift({
test: /vanilla\.virtual\.css/i,
sideEffects: true,
use: getVanillaExtractCssLoaders(
options,
resolvedNextConfig.assetPrefix,
),
});

// vanilla-extract need to emit the css file on both server and client, both during the
// development and production.
// However, Next.js only add MiniCssExtractPlugin on pages dir + client build + production mode.
//
// To simplify the logic at our side, we will add MiniCssExtractPlugin based on
// the "instanceof" check (We will only add our required MiniCssExtractPlugin if
// Next.js hasn't added it yet).
// This also prevent multiple MiniCssExtractPlugin being added (which will cause
// RealContentHashPlugin to panic)
if (
!config.plugins.some((p: any) => p instanceof NextMiniCssExtractPlugin)
) {
// HMR reloads the CSS file when the content changes but does not use
// the new file name, which means it can't contain a hash.
const filename = dev
? 'static/css/[name].css'
: 'static/css/[contenthash].css';

config.plugins.push(
new VanillaExtractPlugin({ outputCss, ...pluginOptions }),
new NextMiniCssExtractPlugin({
filename,
chunkFilename: filename,
// Next.js guarantees that CSS order "doesn't matter", due to imposed
// restrictions:
// 1. Global CSS can only be defined in a single entrypoint (_app)
// 2. CSS Modules generate scoped class names by default and cannot
// include Global CSS (:global() selector).
//
// While not a perfect guarantee (e.g. liberal use of `:global()`
// selector), this assumption is required to code-split CSS.
//
// If this warning were to trigger, it'd be unactionable by the user,
// but likely not valid -- so just disable it.
ignoreOrder: true,
}),
);
}

if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options);
}
config.plugins.push(
new VanillaExtractPlugin({ outputCss, ...pluginOptions }),
);

return config;
},
});
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options);
}

return config;
},
});
};
4 changes: 4 additions & 0 deletions packages/webpack-plugin/next/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"main": "dist/vanilla-extract-webpack-plugin-next.cjs.js",
"module": "dist/vanilla-extract-webpack-plugin-next.esm.js"
}
17 changes: 15 additions & 2 deletions packages/webpack-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,33 @@
"./virtualFileLoader": {
"module": "./virtualFileLoader/dist/vanilla-extract-webpack-plugin-virtualFileLoader.esm.js",
"default": "./virtualFileLoader/dist/vanilla-extract-webpack-plugin-virtualFileLoader.cjs.js"
},
"./next": {
"module": "./next/dist/vanilla-extract-webpack-plugin-next.esm.js",
"default": "./next/dist/vanilla-extract-webpack-plugin-next.cjs.js"
},
"./virtualNextFileLoader": {
"module": "./virtualNextFileLoader/dist/vanilla-extract-webpack-plugin-virtualNextFileLoader.esm.js",
"default": "./virtualNextFileLoader/dist/vanilla-extract-webpack-plugin-virtualNextFileLoader.cjs.js"
}
},
"preconstruct": {
"entrypoints": [
"index.ts",
"loader.ts",
"virtualFileLoader.ts"
"virtualFileLoader.ts",
"next.ts",
"virtualNextFileLoader.ts"
]
},
"files": [
"/dist",
"/loader",
"/virtualFileLoader",
"extracted.js"
"/next",
"/virtualNextFileLoader",
"extracted.js",
"vanilla.virtual.css"
],
"repository": {
"type": "git",
Expand Down
Loading

0 comments on commit 89224fe

Please sign in to comment.