stylex-webpack
Version:
The another Webpack Plugin for Facebook's StyleX
176 lines (171 loc) • 10.3 kB
JavaScript
;
var nextMiniCssExtractPluginExports = require('next/dist/build/webpack/plugins/mini-css-extract-plugin');
var log = require('next/dist/build/output/log');
var browserslist = require('next/dist/compiled/browserslist');
var css = require('next/dist/build/webpack/config/blocks/css');
var index = require('./index');
require.resolve('./stylex.css');
require.resolve('./stylex-virtual.css');
const VIRTUAL_CSS_PATTERN = /stylex\.css|stylex-virtual\.css/;
/** Next.js' precompilation add "__esModule: true", but doesn't add an actual default exports */ // @ts-expect-error -- Next.js fucks something up
const NextMiniCssExtractPlugin = nextMiniCssExtractPluginExports.default;
// Adopted from https://github.com/vercel/next.js/blob/1f1632979c78b3edfe59fd85d8cce62efcdee688/packages/next/build/webpack-config.ts#L60-L72
function getSupportedBrowsers(dir, isDevelopment) {
try {
return browserslist.loadConfig({
path: dir,
env: isDevelopment ? 'development' : 'production'
});
} catch {}
}
function getNextMiniCssExtractPlugin(isDev) {
// Use own MiniCssExtractPlugin to ensure HMR works
// v9 has issues when using own plugin in production
// v10.2.1 has issues when using built-in plugin in development since it
// doesn't bundle HMR files
// v12.1.7 finaly fixes the issue by adding the missing hmr/hotModuleReplacement.js file
if (isDev) {
try {
// Check if hotModuleReplacement exists
require.resolve('next/dist/compiled/mini-css-extract-plugin/hmr/hotModuleReplacement');
return NextMiniCssExtractPlugin;
} catch {
log.warn('Next.js built-in mini-css-extract-plugin is broken, will fallback to "mini-css-extract-plugin"');
// eslint-disable-next-line @typescript-eslint/no-require-imports -- Require only when needed
return require('mini-css-extract-plugin');
}
}
// Always use Next.js built-in MiniCssExtractPlugin in production
return NextMiniCssExtractPlugin;
}
// Adopt from Next.js' getGlobalCssLoader
// https://github.com/vercel/next.js/blob/d61b0761efae09bd9cb1201ff134ed8950d9deca/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L7
function getStyleXVirtualCssLoader(ctx, MiniCssExtractPlugin, postcss) {
const loaders = [];
// Adopt from Next.js' getClientStyleLoader
// https://github.com/vercel/next.js/blob/56d35ede8ed2ab25fa8e29583d4e81e3e76a0e29/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L7
if (!ctx.isServer) {
// https://github.com/vercel/next.js/blob/56d35ede8ed2ab25fa8e29583d4e81e3e76a0e29/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L18
// https://github.com/vercel/next.js/blob/56d35ede8ed2ab25fa8e29583d4e81e3e76a0e29/packages/next/src/build/webpack/config/blocks/css/loaders/client.ts#L3
loaders.push({
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: `${ctx.config.assetPrefix}/_next/`,
esModule: false
}
});
}
// We don't actually use postcss-loader or css-loader to run against
// the stylex css (which doesn't exist yet).
// We use this loader to run against the virtual dummy css.
loaders.push({
// https://github.com/vercel/next.js/blob/0572e218afe130656be53f7367bf18c4ea389f7d/packages/next/build/webpack/config/blocks/css/loaders/global.ts#L29-L38
loader: require.resolve('next/dist/build/webpack/loaders/css-loader/src'),
options: {
// https://github.com/vercel/next.js/blob/88a5f263f11cb55907f0d89a4cd53647ee8e96ac/packages/next/build/webpack/config/blocks/css/index.ts#L142-L147
postcss,
importLoaders: 1,
modules: false
}
});
return loaders;
}
function withStyleX(pluginOptions) {
return (nextConfig = {})=>{
const config = {
...nextConfig,
webpack (config, ctx) {
var // For some reason, Next 11.0.1 has `config.optimization.splitChunks`
// set to `false` when webpack 5 is enabled.
_config, _config_optimization, _config_optimization_splitChunks, _config1;
if (typeof nextConfig.webpack === 'function') {
config = nextConfig.webpack(config, ctx);
}
(_config = config).optimization || (_config.optimization = {});
(_config_optimization = config.optimization).splitChunks || (_config_optimization.splitChunks = {});
(_config_optimization_splitChunks = config.optimization.splitChunks).cacheGroups || (_config_optimization_splitChunks.cacheGroups = {});
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- could be undefined
const useLightningcss = config.experimental?.useLightningcss;
let lazyPostCSSPromise = null;
const postcss = ()=>{
lazyPostCSSPromise || (lazyPostCSSPromise = css.lazyPostCSS(ctx.dir, getSupportedBrowsers(ctx.dir, ctx.dev), undefined, useLightningcss));
return lazyPostCSSPromise;
};
const MiniCssExtractPlugin = getNextMiniCssExtractPlugin(ctx.dev);
// Based on https://github.com/vercel/next.js/blob/88a5f263f11cb55907f0d89a4cd53647ee8e96ac/packages/next/build/webpack/config/helpers.ts#L12-L18
const cssRules = (config.module?.rules?.find((rule)=>typeof rule === 'object' && rule !== null && Array.isArray(rule.oneOf) && rule.oneOf.some((setRule)=>setRule && setRule.test instanceof RegExp && typeof setRule.test.test === 'function' && setRule.test.test('filename.css')))).oneOf;
// Here we matches virtual css file emitted by StyleXPlugin
cssRules?.unshift({
test: VIRTUAL_CSS_PATTERN,
use: getStyleXVirtualCssLoader(ctx, MiniCssExtractPlugin, postcss)
});
(_config1 = config).plugins ?? (_config1.plugins = []);
// StyleX need to emit the css file on both server and client, both during the
// development and production.
// However, Next.js only add MiniCssExtractPlugin on client + production.
//
// 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)=>plugin instanceof MiniCssExtractPlugin)) {
// 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 = ctx.dev ? 'static/css/[name].css' : 'static/css/[contenthash].css';
// Logic adopted from https://github.com/vercel/next.js/blob/143769bc8304423ba2038accf6f10de240733821/packages/next/src/build/webpack/config/blocks/css/index.ts#L606
config.plugins.push(new MiniCssExtractPlugin({
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.
//
// As for StyleX, the CSS is always atomic (so classes are always unique),
// and StyleX Plugin will always sort the css based on media query and pseudo
// selector.
//
// If this warning were to trigger, it'd be unactionable by the user,
// but likely not valid -- so just disable it.
ignoreOrder: true
}));
}
config.plugins.push(new index.StyleXPlugin({
nextjsMode: true,
nextjsAppRouterMode: true,
async transformCss (css) {
const { postcssWithPlugins } = await postcss();
// add from: undefined to avoid source map warning
const result = await postcssWithPlugins.process(css, {
from: undefined
});
if (pluginOptions?.transformCss) {
return pluginOptions.transformCss(result.css);
}
return result.css;
},
...pluginOptions,
stylexOption: {
...pluginOptions?.stylexOption,
dev: ctx.dev
}
}));
return config;
}
};
// https://github.com/vercel/next.js/blob/ad6907a8a37e930639af071203f4ce49a5d69ee5/packages/next/src/build/index.ts#L1723
// Actually, if a custom webpack config is provided, Next.js will always disable parallel bundling
// But we should not take that as an assumption, so we just warn and disable the options
if (config.experimental?.webpackBuildWorker) {
log.warn('[stylex-webpack] "experimental.webpackBuildWorker" is not supported with "stylex-webpack", the option will be disabled.');
config.experimental.webpackBuildWorker = false;
}
return config;
};
}
exports.withStyleX = withStyleX;