UNPKG

@esmx/rspack

Version:

A high-performance Rspack integration for Esmx microfrontend framework, providing Module Linking and SSR capabilities.

496 lines (452 loc) 13.5 kB
import type { Esmx } from '@esmx/core'; import { type LightningcssLoaderOptions, type SwcLoaderOptions, rspack } from '@rspack/core'; import NodePolyfillPlugin from 'node-polyfill-webpack-plugin'; import type RspackChain from 'rspack-chain'; import { type BuildTarget, RSPACK_LOADER, type RspackAppOptions, createRspackApp } from '../rspack'; import { getTargetSetting } from './target-setting'; import type { TargetSetting } from './target-setting'; export type { TargetSetting }; export interface RspackHtmlAppOptions extends RspackAppOptions { /** * CSS output mode configuration * * @default Automatically selected based on environment: * - Production: 'css', outputs CSS to separate files for better caching and parallel loading * - Development: 'js', bundles CSS into JS to support hot module replacement (HMR) for instant style updates * * - 'css': Output CSS to separate CSS files * - 'js': Bundle CSS into JS files and dynamically inject styles at runtime * - false: Disable default CSS processing configuration, requires manual loader rule configuration * * @example * ```ts * // Use environment default configuration * css: undefined * * // Force output to separate CSS files * css: 'css' * * // Force bundle into JS * css: 'js' * * // Custom CSS processing * css: false * ``` */ css?: 'css' | 'js' | false; /** * Custom loader configuration * * Allows replacing default loader implementations, useful for switching to framework-specific loaders * * @example * ```ts * // Use Vue's style-loader * loaders: { * styleLoader: 'vue-style-loader' * } * ``` */ loaders?: Partial<Record<keyof typeof RSPACK_LOADER, string>>; /** * Configure style injection method. For complete options, see: * https://github.com/webpack-contrib/style-loader * * @example * ```ts * styleLoader: { * injectType: 'singletonStyleTag', * attributes: { id: 'app-styles' } * } * ``` */ styleLoader?: Record<string, any>; /** * Configure CSS modules, URL resolution, etc. For complete options, see: * https://github.com/webpack-contrib/css-loader * * @example * ```ts * cssLoader: { * modules: true, * url: false * } * ``` */ cssLoader?: Record<string, any>; /** * Configure Less compilation options. For complete options, see: * https://github.com/webpack-contrib/less-loader * * @example * ```ts * lessLoader: { * lessOptions: { * javascriptEnabled: true, * modifyVars: { '@primary-color': '#1DA57A' } * } * } * ``` */ lessLoader?: Record<string, any>; /** * Automatically inject global style resources. For complete options, see: * https://github.com/yenshih/style-resources-loader * * @example * ```ts * styleResourcesLoader: { * patterns: [ * './src/styles/variables.less', * './src/styles/mixins.less' * ] * } * ``` */ styleResourcesLoader?: Record<string, any>; /** * Configure TypeScript/JavaScript compilation options. For complete options, see: * https://rspack.dev/guide/features/builtin-swc-loader * * @example * ```ts * swcLoader: { * jsc: { * parser: { * syntax: 'typescript', * decorators: true * }, * transform: { * legacyDecorator: true * } * } * } * ``` */ swcLoader?: SwcLoaderOptions; /** * Define compile-time global constants, supports setting different values for different build targets * For complete documentation, see: https://rspack.dev/plugins/webpack/define-plugin * * @example * ```ts * // Unified value * definePlugin: { * 'process.env.APP_ENV': JSON.stringify('production') * } * * // Values for different build targets * definePlugin: { * 'process.env.IS_SERVER': { * server: 'true', * client: 'false' * } * } * ``` */ definePlugin?: Record< string, string | Partial<Record<BuildTarget, string>> >; /** * Set the target runtime environment for the code, affecting code compilation downgrading and polyfill injection * * @example * ```ts * // Global compatible mode * target: 'compatible' * * // Global modern mode * target: 'modern' * * // Global custom targets * target: ['chrome>=89', 'edge>=89', 'firefox>=108', 'safari>=16.4', 'node>=24'] * * // Per-build-target configuration * target: { * client: 'modern', * server: ['node>=24'] * } * ``` */ target?: TargetSetting; } export async function createRspackHtmlApp( esmx: Esmx, options?: RspackHtmlAppOptions ) { options = { ...options, css: options?.css ? options.css : esmx.isProd ? 'css' : 'js' }; return createRspackApp(esmx, { ...options, chain(context) { const { chain, buildTarget, esmx } = context; chain.stats('errors-warnings'); chain.devtool(false); chain.cache(false); configureAssetRules(chain, esmx); chain.module .rule('json') .test(/\.json$/i) .type('json'); configureWorkerRule(chain, esmx, options); configureTypeScriptRule(chain, buildTarget, options); configureOptimization(chain, options); chain.plugin('node-polyfill').use(NodePolyfillPlugin); configureDefinePlugin(chain, buildTarget, options); chain.resolve.extensions.clear().add('...').add('.ts'); configureCssRules(chain, esmx, options); options?.chain?.(context); } }); } function configureAssetRules(chain: RspackChain, esmx: Esmx): void { chain.module .rule('images') .test( /\.(png|jpg|jpeg|gif|svg|bmp|webp|ico|apng|avif|tif|tiff|jfif|pjpeg|pjp|cur)$/i ) .type('asset/resource') .set('generator', { filename: filename(esmx, 'images') }); chain.module .rule('media') .test(/\.(mp4|webm|ogg|mov)$/i) .type('asset/resource') .set('generator', { filename: filename(esmx, 'media') }); chain.module .rule('audio') .test(/\.(mp3|wav|flac|aac|m4a|opus)$/i) .type('asset/resource') .set('generator', { filename: filename(esmx, 'audio') }); chain.module .rule('fonts') .test(/\.(woff|woff2|eot|ttf|otf|ttc)(\?.*)?$/i) .type('asset/resource') .set('generator', { filename: filename(esmx, 'fonts') }); } function configureWorkerRule( chain: RspackChain, esmx: Esmx, options: RspackHtmlAppOptions ): void { chain.module .rule('worker') .test(/\.worker\.(c|m)?(t|j)s$/i) .use('worker-loader') .loader( options.loaders?.workerRspackLoader ?? RSPACK_LOADER.workerRspackLoader ) .options({ esModule: false, filename: `${esmx.name}/workers/[name].[contenthash]${esmx.isProd ? '.final' : ''}.js` }); } function configureTypeScriptRule( chain: RspackChain, buildTarget: BuildTarget, options: RspackHtmlAppOptions ): void { const targets = getTargetSetting(options?.target, buildTarget); chain.module .rule('typescript') .test(/\.(ts|mts)$/i) .use('swc-loader') .loader( options.loaders?.builtinSwcLoader ?? RSPACK_LOADER.builtinSwcLoader ) .options({ env: { targets, ...options?.swcLoader?.env }, jsc: { parser: { syntax: 'typescript', ...options?.swcLoader?.jsc?.parser }, ...options?.swcLoader?.jsc }, ...options?.swcLoader } as SwcLoaderOptions) .end() .type('javascript/auto'); } function configureOptimization( chain: RspackChain, options: RspackHtmlAppOptions ): void { chain.optimization .minimizer('swc-js-minimizer') .use(rspack.SwcJsMinimizerRspackPlugin, [ { minimizerOptions: { format: { comments: false } } } ]); chain.optimization .minimizer('lightningcss-minimizer') .use(rspack.LightningCssMinimizerRspackPlugin, [ { minimizerOptions: { targets: getTargetSetting(options?.target, 'client'), errorRecovery: false } } ]); } function configureDefinePlugin( chain: RspackChain, buildTarget: BuildTarget, options: RspackHtmlAppOptions ): void { if (options.definePlugin) { const defineOptions: Record<string, string> = {}; Object.entries(options.definePlugin).forEach(([name, value]) => { const targetValue = typeof value === 'string' ? value : value[buildTarget as keyof typeof value]; if (typeof targetValue === 'string' && name !== targetValue) { defineOptions[name] = targetValue; } }); if (Object.keys(defineOptions).length) { chain.plugin('define').use(rspack.DefinePlugin, [defineOptions]); } } } function configureCssRules( chain: RspackChain, esmx: Esmx, options: RspackHtmlAppOptions ): void { if (options.css === false) { return; } if (options.css === 'js') { configureCssInJS(chain, esmx, options); return; } configureCssExtract(chain, options); } function configureCssInJS( chain: RspackChain, esmx: Esmx, options: RspackHtmlAppOptions ): void { chain.module .rule('css') .test(/\.css$/) .use('style-loader') .loader(options.loaders?.styleLoader ?? RSPACK_LOADER.styleLoader) .options(options.styleLoader ?? {}) .end() .use('css-loader') .loader(options.loaders?.cssLoader ?? RSPACK_LOADER.cssLoader) .options(options.cssLoader ?? {}) .end() .use('lightning-css-loader') .loader( options.loaders?.lightningcssLoader ?? RSPACK_LOADER.lightningcssLoader ) .options({ targets: getTargetSetting(options?.target, 'client'), minify: esmx.isProd } as LightningcssLoaderOptions) .end() .type('javascript/auto'); const lessRule = chain.module .rule('less') .test(/\.less$/) .use('style-loader') .loader(options.loaders?.styleLoader ?? RSPACK_LOADER.styleLoader) .options(options.styleLoader ?? {}) .end() .use('css-loader') .loader(options.loaders?.cssLoader ?? RSPACK_LOADER.cssLoader) .options(options.cssLoader ?? {}) .end() .use('lightning-css-loader') .loader( options.loaders?.lightningcssLoader ?? RSPACK_LOADER.lightningcssLoader ) .options({ targets: getTargetSetting(options?.target, 'client'), minify: esmx.isProd } as LightningcssLoaderOptions) .end() .use('less-loader') .loader(options.loaders?.lessLoader ?? RSPACK_LOADER.lessLoader) .options(options.lessLoader ?? {}) .end(); if (options.styleResourcesLoader) { lessRule .use('style-resources-loader') .loader( options.loaders?.styleResourcesLoader ?? RSPACK_LOADER.styleResourcesLoader ) .options(options.styleResourcesLoader); } lessRule.type('javascript/auto'); } function configureCssExtract( chain: RspackChain, options: RspackHtmlAppOptions ): void { chain.set('experiments', { ...(chain.get('experiments') ?? {}), css: true }); const experiments = chain.get('experiments'); if (!experiments || !experiments.css) { return; } const lessRule = chain.module .rule('less') .test(/\.less$/) .use('less-loader') .loader(options.loaders?.lessLoader ?? RSPACK_LOADER.lessLoader) .options(options.lessLoader ?? {}) .end(); if (options.styleResourcesLoader) { lessRule .use('style-resources-loader') .loader( options.loaders?.styleResourcesLoader ?? RSPACK_LOADER.styleResourcesLoader ) .options(options.styleResourcesLoader); } lessRule.type('css'); } function filename(esmx: Esmx, name: string, ext = '[ext]') { return esmx.isProd ? `${name}/[name].[contenthash:8].final${ext}` : `${name}/[path][name]${ext}`; }