UNPKG

stylex-webpack

Version:

The another Webpack Plugin for Facebook's StyleX

235 lines (229 loc) 13.4 kB
'use strict'; 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;