stylex-webpack
Version:
The another Webpack Plugin for Facebook's StyleX
235 lines (229 loc) • 13.4 kB
JavaScript
;
var stylexBabelPlugin = require('@stylexjs/babel-plugin');
var path = require('node:path');
var process = require('node:process');
var identity = require('foxts/identity');
var version = "0.4.13";
const PLUGIN_NAME = 'stylex';
const VIRTUAL_ENTRYPOINT_CSS_PATH = require.resolve('./stylex.css');
require.resolve('./stylex-virtual.css');
const VIRTUAL_CSS_PATTERN = /stylex\.css|stylex-virtual\.css/;
const VIRTUAL_STYLEX_CSS_DUMMY_IMPORT_PATTERN = /stylex-virtual\.css/;
const STYLEX_CHUNK_NAME = '_stylex-webpack-generated';
const INCLUDE_REGEXP = /\.[cm]?[jt]sx?$/;
const BUILD_INFO_STYLEX_KEY = '~stylex_webpack_stylex_rules';
// https://github.com/vercel/next.js/blob/ad6907a8a37e930639af071203f4ce49a5d69ee5/packages/next/src/shared/lib/constants.ts#L7
const NEXTJS_COMPILER_NAMES = {
client: 'client',
server: 'server',
edgeServer: 'edge-server'
};
function isNextJsCompilerName(name) {
if (name == null) return false;
return NEXTJS_COMPILER_NAMES[name] === name;
}
function _define_property(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
const stylexLoaderPath = require.resolve('./stylex-loader');
const stylexVirtualCssLoaderPath = require.resolve('./stylex-virtual-css-loader');
function getStyleXRules(stylexRules, useCSSLayers) {
if (stylexRules.size === 0) {
return null;
}
// Take styles for the modules that were included in the last compilation.
const allRules = Array.from(stylexRules.values()).flat();
return stylexBabelPlugin.processStylexRules(allRules, useCSSLayers);
}
class StyleXPlugin {
apply(compiler) {
var _compiler_options_optimization_splitChunks;
// If splitChunk is enabled, we create a dedicated chunk for stylex css
if (!compiler.options.optimization.splitChunks) {
throw new Error([
'You don\'t have "optimization.splitChunks" enabled.',
'"optimization.splitChunks" should be enabled for "stylex-webpack" to function properly.'
].join(' '));
}
(_compiler_options_optimization_splitChunks = compiler.options.optimization.splitChunks).cacheGroups ?? (_compiler_options_optimization_splitChunks.cacheGroups = {});
compiler.options.optimization.splitChunks.cacheGroups[STYLEX_CHUNK_NAME] = {
name: STYLEX_CHUNK_NAME,
test: VIRTUAL_CSS_PATTERN,
type: 'css/mini-extract',
chunks: 'all',
enforce: true
};
// const IS_RSPACK = Object.prototype.hasOwnProperty.call(compiler.webpack, 'rspackVersion');
// stylex-loader adds virtual css import (which triggers virtual-loader)
// This prevents "stylex.virtual.css" files from being tree shaken by forcing
// "sideEffects" setting.
// TODO-RSPACK: rspack does support normalModuleFactory, we need to test this out
compiler.hooks.normalModuleFactory.tap(PLUGIN_NAME, (nmf)=>{
nmf.hooks.createModule.tap(PLUGIN_NAME, (createData)=>{
const modPath = createData.matchResource ?? createData.resourceResolveData?.path;
if (modPath === VIRTUAL_ENTRYPOINT_CSS_PATH) {
var _createData;
(_createData = createData).settings ?? (_createData.settings = {});
createData.settings.sideEffects = true;
}
});
});
// compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
// compilation.dependencyTemplates.set(
// CssLocalIdentifierDependency,
// new CssLocalIdentifierDependency.Template()
// );
// });
const { Compilation, NormalModule, sources } = compiler.webpack;
const { RawSource } = sources;
const meta = JSON.stringify({
name: 'stylex-webpack',
packageVersion: version,
opt: this.loaderOption
});
const logger = compiler.getInfrastructureLogger(PLUGIN_NAME);
// Apply loader to JS modules
compiler.hooks.make.tap(PLUGIN_NAME, (compilation)=>{
compilation.hooks.chunkHash.tap(PLUGIN_NAME, (_, hash)=>hash.update(meta));
NormalModule.getCompilationHooks(compilation).loader.tap(PLUGIN_NAME, (_loaderContext, mod)=>{
const extname = path.extname(mod.matchResource || mod.resource);
logger.debug(`attaching stylex-loader to ${mod.matchResource || mod.resource}`);
if (INCLUDE_REGEXP.test(extname)) {
// We use .push() here instead of .unshift()
// Webpack usually runs loaders in reverse order and we want to ideally run
// our loader before anything else.
mod.loaders.push({
loader: stylexLoaderPath,
options: this.loaderOption,
ident: null,
type: null
});
} else if (VIRTUAL_STYLEX_CSS_DUMMY_IMPORT_PATTERN.test(mod.matchResource || mod.resource)) {
mod.loaders.push({
loader: stylexVirtualCssLoaderPath,
ident: null,
type: null
});
}
});
/**
* Next.js call webpack compiler through "runCompiler": https://github.com/vercel/next.js/blob/c0c75e4aaa8ece2c9e789e2e3f150d7487b60bbc/packages/next/src/build/compiler.ts#L39
*
* The "runCompiler" funtion is invoked by "webpackBuildImpl" function: https://github.com/vercel/next.js/blob/ad6907a8a37e930639af071203f4ce49a5d69ee5/packages/next/src/build/webpack-build/impl.ts#L203
*
* The "webpackBuildImpl" function accepts "compilerName" parameter, and is invoked by "webpackBuild" function: https://github.com/vercel/next.js/blob/c0c75e4aaa8ece2c9e789e2e3f150d7487b60bbc/packages/next/src/build/webpack-build/index.ts#L124
* When build worker is enabled, the "compilerName" parameter is set to either "client", "server" or "edge-server". If build worker is disabled,
* the "compilerName" parameter is always "null".
*
* When build worker is disabled, the multi-stage build is managed by "webpackBuildImpl" function itself: https://github.com/vercel/next.js/blob/ad6907a8a37e930639af071203f4ce49a5d69ee5/packages/next/src/build/webpack-build/impl.ts#L203
* It will first run "server" compiler, then "edge-server" compiler, and finally "client" compiler.
*
* The "webpackBuildImpl" function is invoked by "webpackBuild" function: https://github.com/vercel/next.js/blob/c0c75e4aaa8ece2c9e789e2e3f150d7487b60bbc/packages/next/src/build/webpack-build/index.ts#L124
*
* If build worker is enabled, the multi-stage build is managed by the build entrypoint, and the "client", "server" and "edge-server" compilerName
* is passed to "webpackBuildImpl" through "webpackBuild" function:
* https://github.com/vercel/next.js/blob/c0c75e4aaa8ece2c9e789e2e3f150d7487b60bbc/packages/next/src/build/index.ts#L905
* https://github.com/vercel/next.js/blob/c0c75e4aaa8ece2c9e789e2e3f150d7487b60bbc/packages/next/src/build/index.ts#L1796
*
* Note that, if a custom webpack config is provided, Next.js will always disable build worker: https://github.com/vercel/next.js/blob/c0c75e4aaa8ece2c9e789e2e3f150d7487b60bbc/packages/next/src/build/index.ts#L1723
* We will not take that as an assumption. We already overwrite "nextConfig.experimental.webpackBuildWorker" to false in the Next.js plugin.
*
* Now all compiler instances are running in the same process, we can use a global variable to track stylex rules from different compilers.
*
* Back to "runCompiler". "runCompiler" accepts webpack configurations which is created by "getBaseWebpackConfig" function: https://github.com/vercel/next.js/blob/ad6907a8a37e930639af071203f4ce49a5d69ee5/packages/next/src/build/webpack-build/impl.ts#L128
*
* Inside "getBaseWebpackConfig" function, there is a "buildConfiguration" function: https://github.com/vercel/next.js/blob/ad6907a8a37e930639af071203f4ce49a5d69ee5/packages/next/src/build/webpack-config.ts#L2464
* Inside "buildConfiguration" function there is a curried "base" function: https://github.com/vercel/next.js/blob/c0c75e4aaa8ece2c9e789e2e3f150d7487b60bbc/packages/next/src/build/webpack/config/index.ts#L73
* Inside the "base" function, the compiler name is attached to the webpack configuration: https://github.com/vercel/next.js/blob/c0c75e4aaa8ece2c9e789e2e3f150d7487b60bbc/packages/next/src/build/webpack/config/blocks/base.ts#L24
*/ compilation.hooks.finishModules.tap(PLUGIN_NAME, (modules)=>{
for (const mod of modules){
if (mod.buildInfo && BUILD_INFO_STYLEX_KEY in mod.buildInfo) {
const stylexBuildInfo = mod.buildInfo[BUILD_INFO_STYLEX_KEY];
if (typeof stylexBuildInfo === 'object' && stylexBuildInfo != null && 'resourcePath' in stylexBuildInfo && 'stylexRules' in stylexBuildInfo && typeof stylexBuildInfo.resourcePath === 'string') {
logger.debug(`collecting stylex rules from ${stylexBuildInfo.resourcePath}'s build info`);
this.stylexRules.set(stylexBuildInfo.resourcePath, stylexBuildInfo.stylexRules);
}
}
}
if (this.loaderOption.nextjsMode && this.loaderOption.nextjsAppRouterMode && isNextJsCompilerName(compiler.name)) {
var _globalThis;
(_globalThis = globalThis).__stylex_nextjs_global_registry__ ?? (_globalThis.__stylex_nextjs_global_registry__ = new Map());
globalThis.__stylex_nextjs_global_registry__.set(compiler.name, this.stylexRules);
}
});
compilation.hooks.processAssets.tapPromise({
name: PLUGIN_NAME,
stage: Compilation.PROCESS_ASSETS_STAGE_PRE_PROCESS
}, async (assets)=>{
if (this.loaderOption.nextjsMode && this.loaderOption.nextjsAppRouterMode && compiler.name === NEXTJS_COMPILER_NAMES.client) {
const globalRegistry = globalThis.__stylex_nextjs_global_registry__;
if (globalRegistry != null) {
// now we merge all collected rules from other compilers
globalRegistry.forEach((rules)=>{
rules.forEach((rule, resourcePath)=>{
this.stylexRules.set(resourcePath, rule);
});
});
}
}
const stylexCSS = getStyleXRules(this.stylexRules, this.useCSSLayers);
if (stylexCSS == null) {
return;
}
const finalCss = await this.transformCss(stylexCSS);
const stylexChunk = compilation.namedChunks.get(STYLEX_CHUNK_NAME);
if (!stylexChunk) return;
// Let's find the css file that belongs to the stylex chunk
const stylexChunkCssAssetNames = Object.keys(assets).filter((assetName)=>stylexChunk.files.has(assetName) && assetName.endsWith('.css'));
if (stylexChunkCssAssetNames.length === 0) {
return;
}
if (stylexChunkCssAssetNames.length > 1) {
logger.warn('Multiple CSS assets found for the stylex chunk. This should not happen. Please report this issue.');
}
const stylexAssetName = stylexChunkCssAssetNames[0];
compilation.updateAsset(stylexAssetName, ()=>new RawSource(finalCss), {
minimized: false
});
});
});
}
constructor({ stylexImports = [
'stylex',
'@stylexjs/stylex'
], useCSSLayers = false, stylexOption = {}, nextjsMode = false, nextjsAppRouterMode = false, transformCss = identity.identity } = {}){
_define_property(this, "stylexRules", new Map());
_define_property(this, "useCSSLayers", void 0);
_define_property(this, "loaderOption", void 0);
_define_property(this, "transformCss", void 0);
this.useCSSLayers = useCSSLayers;
this.loaderOption = {
stylexImports,
stylexOption: {
dev: process.env.NODE_ENV === 'development',
// useRemForFontSize: true,
enableFontSizePxToRem: true,
runtimeInjection: false,
// genConditionalClasses: true,
enableInlinedConditionalMerge: true,
treeshakeCompensation: true,
importSources: stylexImports,
...stylexOption
},
nextjsMode,
nextjsAppRouterMode
};
this.transformCss = transformCss;
}
}
exports.StyleXPlugin = StyleXPlugin;