Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(next-plugin): build not outputting css on windows #1180

Merged
merged 3 commits into from
Sep 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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