@stylexswc/nextjs-plugin
Version:
StyleX plugin for Next.js powered by a Rust NAPI-RS/SWC compiler. Supports Webpack, Rspack, and Turbopack builds with CSS extraction.
239 lines (238 loc) • 12 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const browserslist_1 = __importDefault(require("next/dist/compiled/browserslist"));
const css_1 = require("next/dist/build/webpack/config/blocks/css");
const get_rspack_1 = require("next/dist/shared/lib/get-rspack");
const rspack_plugin_1 = __importStar(require("@stylexswc/rspack-plugin"));
const next_rspack_1 = __importDefault(require("next-rspack"));
// Adopted from https://github.com/vercel/next.js/blob/1f1632979c78b3edfe59fd85d8cce62efcdee688/packages/next/build/webpack-config.ts#L60-L72
const getSupportedBrowsers = (dir, isDevelopment) => {
try {
return browserslist_1.default.loadConfig({
path: dir,
env: isDevelopment ? 'development' : 'production',
});
}
catch {
// Ignore
}
};
/**
* Resolves CssExtractRspackPlugin from the exact module instance Next.js uses
* (`next-rspack/rspack-core`), so the `instanceof` dedup check below cannot be
* defeated by a second copy of @rspack/core
*/
const getCssExtractPlugin = () => {
const rspackCore = (0, get_rspack_1.getRspackCore)();
const plugin = rspackCore.rspack?.CssExtractRspackPlugin ?? rspackCore.CssExtractRspackPlugin;
if (!plugin) {
throw new Error('@stylexswc/nextjs-plugin/rspack: CssExtractRspackPlugin not found in next-rspack/rspack-core.');
}
return plugin;
};
// 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, cssExtractPlugin, 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#L18
if (!ctx.isServer) {
loaders.push({
loader: cssExtractPlugin.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;
}
const withStyleX = (pluginOptions) => (nextConfig = {}) => {
// Scoped per `withStyleX(...)` call rather than module-level, so it doesn't
// leak across unrelated Next.js configs sharing this process (e.g. a
// monorepo building multiple apps, or repeated calls in tests).
let count = 0;
// `withRspack` switches Next.js to the Rspack bundler for this config
// (sets NEXT_RSPACK); applied to the final config object so users don't
// have to compose `next-rspack` themselves
return (0, next_rspack_1.default)({
...nextConfig,
webpack(config, ctx) {
if (!process.env.NEXT_RSPACK) {
throw new Error([
'@stylexswc/nextjs-plugin/rspack requires Next.js to run with Rspack.',
'Run `next dev`/`next build` without the `--webpack` or `--turbopack` flags',
'(set NEXT_RSPACK=true for `next start`),',
'or use `@stylexswc/nextjs-plugin` for the default webpack bundler.',
].join(' '));
}
if (typeof nextConfig.webpack === 'function') {
config = nextConfig.webpack(config, ctx);
}
const { buildId, dev, isServer } = ctx;
if (pluginOptions?.rsOptions?.debug || process.env.STYLEX_DEBUG) {
console.log([
'!!!GETTING RSPACK CONFIG!!!',
'======================',
`Count: ${++count}`,
`Build ID: ${buildId}`,
`Server: ${isServer}`,
`Env: ${dev ? 'dev' : 'prod'}`,
].join('\n'));
}
config.optimization ||= {};
config.optimization.splitChunks ||= {};
config.optimization.splitChunks.cacheGroups ||= {};
const extractCSS = pluginOptions?.extractCSS ?? true;
config.plugins ??= [];
let lazyPostCSSPromise = null;
const postcss = () => {
lazyPostCSSPromise ||= (0, css_1.lazyPostCSS)(ctx.dir, getSupportedBrowsers(ctx.dir, ctx.dev), nextConfig?.experimental?.disablePostcssPresetEnv, nextConfig?.experimental?.useLightningcss);
return lazyPostCSSPromise;
};
if (extractCSS) {
const CssExtractPlugin = getCssExtractPlugin();
// Based on https://github.com/vercel/next.js/blob/88a5f263f11cb55907f0d89a4cd53647ee8e96ac/packages/next/build/webpack/config/helpers.ts#L12-L18
const cssContainerRule = 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')));
const cssRules = cssContainerRule?.oneOf;
if (!cssRules) {
throw new Error([
"@stylexswc/nextjs-plugin/rspack: could not find Next.js' css oneOf rules",
'in the Rspack config. StyleX CSS extraction cannot be wired up —',
'this likely indicates an incompatible Next.js version. Please report this issue.',
].join(' '));
}
// Here we matches virtual css file emitted by StyleXPlugin
cssRules.unshift({
test: rspack_plugin_1.VIRTUAL_CSS_PATTERN,
use: getStyleXVirtualCssLoader(ctx, CssExtractPlugin, postcss),
});
// StyleX needs to emit the css file on both server and client, both during
// the development and production.
// However, Next.js only adds CssExtractRspackPlugin on the client.
//
// The instanceof check prevents multiple extract plugins from being added
// (which would cause RealContentHashPlugin to panic)
if (!config.plugins.some((plugin) => plugin instanceof CssExtractPlugin)) {
// 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';
config.plugins.push(new CssExtractPlugin({
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,
}));
}
}
// Packages in transpilePackages ship untransformed source (Next requirement
// for StyleX-authoring packages), so they are exactly the node_modules
// packages the stylex-loader must process
const stylexPackages = Array.from(new Set([
...(pluginOptions?.stylexPackages ?? rspack_plugin_1.DEFAULT_STYLEX_PACKAGES),
...(nextConfig.transpilePackages ?? []),
]));
config.plugins.push(new rspack_plugin_1.default({
...pluginOptions,
stylexPackages,
rsOptions: {
...pluginOptions?.rsOptions,
dev: ctx.dev,
},
// Enforce nextjsMode to true
nextjsMode: true,
...(extractCSS
? {
async transformCss(css, filePath) {
const { postcssWithPlugins } = await postcss();
const result = await postcssWithPlugins.process(css, {
from: filePath,
map: {
inline: false,
annotation: false,
},
});
if (typeof pluginOptions?.transformCss === 'function') {
return pluginOptions.transformCss(result.css, filePath);
}
return result.css;
},
}
: { transformCss: undefined }),
}));
return config;
},
});
};
exports.default = withStyleX;
module.exports = withStyleX;
module.exports.default = withStyleX;