Package Exports
- rspack-unocss-plugin
- rspack-unocss-plugin/loader
Readme
rspack-unocss-plugin
A native Rspack integration for UnoCSS that generates virtual UnoCSS styles and feeds them into Rspack's native CSS pipeline.
This package is designed for projects that want UnoCSS to work as a first-class Rspack plugin instead of going through the webpack compatibility integration. It provides uno.css virtual modules, layer-specific CSS entries, config loading, filesystem extraction, transformer execution, and automatic CSS rule integration for Rspack 1.5+ and Rspack 2.x.
Status: this project is currently a focused native-integration prototype. The core virtual CSS, extraction, transformer, layer, watch dependency, benchmark, and example workflows are implemented. Some advanced parity features from the official webpack integration are still being refined.
Why This Plugin Exists
UnoCSS already provides @unocss/webpack/rspack, but that integration is implemented through the webpack-oriented unplugin layer. In Rspack + Vue SFC projects, especially when native CSS handling is enabled, webpack-style assumptions can break down. A representative failure mode is:
ENOENT: no such file or directory, open '.../ExampleCounter.vue?vue&type=style&index=0&lang=css&scoped=...'That path is not a real file on disk. It is a Vue SFC style sub-request generated by the loader pipeline. The official Rspack entry reuses webpack integration logic that may inspect module.resource and attempt filesystem reads. When the resource contains a query such as ?vue&type=style, treating it as a real path causes invalid reads and build/dev-server failures.
This plugin takes a Rspack-native approach instead:
- Resolve
uno.cssanduno:*layer requests to Rspack virtual modules. - Generate CSS through UnoCSS directly and write it into Rspack's virtual module store.
- Let Rspack native CSS process the generated CSS instead of relying on webpack CSS assumptions.
- Avoid reading Vue style sub-requests as real files.
- Keep Vue, native CSS, scoped styles, CSS Modules, and extraction behavior on the standard Rspack pipeline.
The result is not intended to replace UnoCSS itself. UnoCSS still owns scanning, presets, rules, variants, shortcuts, transformers, layers, and CSS generation. Rspack owns bundling, native CSS parsing, CSS assets, and optional Lightning CSS minimization.
Core Value
- Native Rspack semantics: Uses
rspack.experiments.VirtualModulesPlugin,rspack.NormalModuleReplacementPlugin, and Rspack plugin hooks instead of relying on webpack-specific module-resource behavior. - Fixes query-resource pitfalls: The loader operates on the source Rspack passes in and skips requests carrying a query (such as
?vue&type=style), avoiding Vue SFC style sub-requestENOENTissues. - Works with native CSS: Sends generated UnoCSS output through Rspack's native CSS pipeline via
type: "css/auto". - Simple application usage: Users import
uno.cssfrom an entry file and register one Rspack plugin. - Layer-aware CSS output: Supports full CSS and layer-specific virtual modules such as
uno:utilities.css. - Ecosystem bridge: Gives Rspack users a more direct UnoCSS integration while preserving UnoCSS config and transformer APIs.
- Benchmarkable design: Includes a benchmark workspace and GitHub Actions workflow for comparing this plugin with
@unocss/webpack/rspack.
Features
Implemented Modules
| Module | Status | Core capabilities | Main files |
|---|---|---|---|
| Plugin entry | Implemented | Rspack plugin class, option resolution, virtual module setup, request replacement, loader/CSS rule injection, processAssets generation |
src/index.ts |
| Context | Implemented | UnoCSS generator creation, config loading, per-module token caches, inline/filesystem content extraction, CSS generation | src/context.ts |
| Loader | Implemented | Runs transformers and token extraction on modules in the build graph, skips query sub-requests and virtual CSS | src/loader.ts |
| Registry | Implemented | Connects the loader to the plugin's UnoCSS context via a per-plugin id | src/registry.ts |
| Virtual CSS | Implemented | uno.css, virtual:uno.css, uno:<layer>.css, query-tolerant matching, deterministic paths, layer/hash placeholders |
src/virtual.ts |
| Transformers | Implemented | Runs UnoCSS source transformers in pre, default, and post order, rewrites source, combines maps, supports idFilter and skip comments |
src/transformers.ts |
| Options | Implemented | Public plugin options and resolved internal options | src/types.ts |
| Tests | Implemented | Virtual CSS matching, placeholders, transformer rewriting, source filter, and per-module extraction coverage | test |
| Example app | Implemented | Vue + Rspack + native CSS + UnoCSS plugin usage | example |
| Benchmark | Implemented | Local-vs-official plugin comparison with generated Vue fixtures and CI summary output | Benchmark, .github/workflows/benchmark.yml |
Plugin Capabilities
- Virtual module replacement: Replaces static imports for
uno.css,virtual:uno.css,uno:preflights.css,uno:default.css,uno:shortcuts.css, anduno:utilities.csswith virtual CSS module paths. - Dynamic layer path creation: Handles layer requests matching
^(?:virtual:)?uno(?::(.+))?\.css(?:\?.*)?$, maps them to deterministic internal paths, and registers unknown layers as virtual modules on first sight. - Automatic CSS rule injection: Adds a fallback
{ test: /\.css$/, type: "css/auto" }rule when no existing CSS rule is detected, optionally routed throughbuiltin:lightningcss-loader. - Native CSS warning: Emits an infrastructure warning if
experiments.cssis not enabled and no explicit CSS handling is expected. - Config loading: Loads
unocss.config.*oruno.config.*through@unocss/config, or accepts an inline UnoCSS config object. - Loader-based extraction: A Rspack loader scans modules as they pass through the build graph, with default extensions for Vue, Svelte, Astro, JSX, TSX, JavaScript, TypeScript, HTML, Markdown, and MDX, and per-module token caches for fine-grained watch rebuilds.
- Custom include/exclude: Supports string glob includes and RegExp includes/excludes for focused extraction.
- UnoCSS
content.filesystem: Includes filesystem patterns declared by UnoCSS config. - UnoCSS
content.inline: Extracts tokens from inline content entries, including async content functions. - Transformer pipeline: Runs configured UnoCSS transformers in
pre/default/postorder, rewriting module source and forwarding combined source maps. - Placeholder-based generation: Replaces layer/hash placeholders in emitted CSS assets during
processAssets, invalidating chunk hashes so watch mode re-emits changed CSS. - Production minification: Uses
uno.generate(tokens, { minify })based on Rspack production mode. - Watch dependencies: Registers config files and
content.filesystemfiles as compilation dependencies and reloads config on change when watch mode is enabled.
Resolved Architecture Notes
- Module-graph extraction: Tokens are extracted by a Rspack loader as modules pass through the build graph, with per-module token caches. Watch rebuilds only re-run changed modules instead of rescanning the whole filesystem.
- Transformer source maps: UnoCSS transformers now rewrite the actual module source and their source maps are combined and forwarded to Rspack, so variant groups and directives are reflected at runtime with accurate maps.
- Generated CSS via placeholders: Virtual CSS modules carry layer/hash placeholders that flow through Rspack's native CSS pipeline and are replaced with the real generated CSS during
processAssets. The hash placeholder invalidates the chunk so watch mode re-emits changed CSS. - Dynamic layer registration: Any static
uno:<layer>.cssrequest is registered as a virtual module on first sight, so layers beyond the common set work without manual configuration. - Optional Lightning CSS: Set
lightningcss: true(or pass loader options) to route the fallback CSS rule throughbuiltin:lightningcss-loader.
Current Limitations
- Layer virtual modules must still be imported with static string literals. Fully dynamic specifiers such as
import(`uno:${name}.css`)whose layer name is only known at runtime cannot be resolved by any static bundler. - PostCSS is intentionally not part of the default path. Native CSS, the Lightning CSS minimizer, and the optional
lightningcssloader cover common cases. Users who require PostCSS-specific behavior should configurepostcss-loaderexplicitly in their Rspack CSS pipeline.
Installation
This repository is currently organized as a pnpm workspace. Inside this workspace, examples use the package through workspace:*.
pnpm add rspack-unocss-plugin unocssPeer dependency:
pnpm add -D @rspack/coreFor Vue projects, install the Rspack Vue loader as usual:
pnpm add -D rspack-vue-loader @rspack/cliAdd the UnoCSS presets you need, for example:
pnpm add -D @unocss/preset-wind4Quick Start
1. Add the Plugin
import { defineConfig } from "@rspack/cli";
import { rspack } from "@rspack/core";
import { VueLoaderPlugin } from "rspack-vue-loader";
import { UnoCSSRspackNativePlugin } from "rspack-unocss-plugin";
export default defineConfig({
entry: {
index: "./src/main.ts",
},
experiments: {
css: true,
},
module: {
rules: [
{
test: /\.vue$/,
loader: "rspack-vue-loader",
options: {
experimentalInlineMatchResource: true,
},
},
],
},
plugins: [
new UnoCSSRspackNativePlugin(),
new VueLoaderPlugin(),
new rspack.HtmlRspackPlugin({
template: "./index.html",
}),
],
});2. Import Virtual CSS
import { createApp } from "vue";
import "uno.css";
import App from "./App.vue";
createApp(App).mount("#app");3. Configure UnoCSS
import { defineConfig } from "unocss";
import presetWind4 from "@unocss/preset-wind4";
export default defineConfig({
presets: [presetWind4()],
});4. Build or Start Dev Server
pnpm rspack build
pnpm rspack devIn this repository, you can run the example with:
pnpm example:build
pnpm example:devConfiguration
Options
import type { UserConfig } from "unocss";
import type { LightningcssLoaderOptions } from "@rspack/core";
export interface UnoCSSRspackNativePluginOptions {
configOrPath?: string | UserConfig;
defaults?: UserConfig;
root?: string;
include?: Array<string | RegExp>;
exclude?: Array<string | RegExp>;
virtualModuleId?: string;
autoCssRule?: boolean;
lightningcss?: boolean | LightningcssLoaderOptions;
watch?: boolean;
}| Option | Default | Description |
|---|---|---|
configOrPath |
Auto-detected config | UnoCSS config object or config file path. |
defaults |
undefined |
Defaults passed to UnoCSS generator creation. |
root |
Rspack compiler context | Project root used for config resolution and filesystem scanning. |
include |
Default source extensions | Glob strings or regular expressions used to include source files. |
exclude |
CSS, node_modules, .git |
Strings or regular expressions used to exclude files from extraction. |
virtualModuleId |
"uno.css" |
Main virtual CSS module ID. |
autoCssRule |
true |
Injects a fallback Rspack native CSS rule if no CSS rule exists. |
lightningcss |
false |
Routes the fallback CSS rule through builtin:lightningcss-loader. |
watch |
true |
Registers config/filesystem dependencies and reloads config on change. |
Focused Extraction
Use include to limit scanning in large projects:
new UnoCSSRspackNativePlugin({
include: ["src/**/*.{vue,ts,tsx,html}"],
});Regular expressions are matched against both absolute paths and paths relative to root:
new UnoCSSRspackNativePlugin({
include: [/src\/.*\.(vue|ts)$/],
});Explicit Config Path
new UnoCSSRspackNativePlugin({
configOrPath: "./unocss.config.ts",
});Inline UnoCSS Config
new UnoCSSRspackNativePlugin({
configOrPath: {
shortcuts: {
btn: "rounded px-4 py-2 font-medium",
},
},
});Manual CSS Rule Control
By default, the plugin adds a fallback native CSS rule when no CSS rule is present:
{
test: /\.css$/,
type: "css/auto",
}Disable this behavior if your project owns all CSS handling explicitly:
new UnoCSSRspackNativePlugin({
autoCssRule: false,
});If autoCssRule is disabled, make sure generated virtual CSS can still be processed by Rspack:
export default defineConfig({
experiments: {
css: true,
},
module: {
rules: [
{
test: /\.css$/,
type: "css/auto",
},
],
},
});Lightning CSS and PostCSS
By default the plugin relies on Rspack native CSS plus LightningCssMinimizerRspackPlugin. To run the fallback CSS rule through builtin:lightningcss-loader for transform/prefix/minify, enable lightningcss:
new UnoCSSRspackNativePlugin({
lightningcss: true,
});Pass an options object to forward loader options:
new UnoCSSRspackNativePlugin({
lightningcss: {
targets: ["last 2 versions", "> 0.2%", "not dead"],
},
});PostCSS is never enabled by default. If you need PostCSS-specific behavior, disable autoCssRule and configure postcss-loader yourself:
new UnoCSSRspackNativePlugin({ autoCssRule: false });
// in module.rules
{
test: /\.css$/,
type: "css/auto",
use: ["postcss-loader"],
}Layer Virtual Modules
The plugin supports UnoCSS layer entry points:
import "uno.css";
import "uno:preflights.css";
import "uno:default.css";
import "uno:shortcuts.css";
import "uno:utilities.css";Supported request forms include:
import "uno.css";
import "virtual:uno.css";
import "uno:utilities.css";
import "virtual:uno:utilities.css";Layer virtual modules must be imported with static string literals. Any static uno:<layer>.css specifier is registered on first sight, but a fully dynamic specifier whose layer name is only known at runtime cannot be resolved by a static bundler.
Feature Demonstration
Full Virtual CSS
<template>
<main class="min-h-screen bg-slate-950 p-8 text-slate-100">
<h1 class="text-4xl font-bold tracking-tight">Rspack + UnoCSS</h1>
</main>
</template>With import "uno.css", the plugin extracts the class tokens, generates UnoCSS output, writes it to a virtual CSS module, and lets Rspack emit the final CSS asset.
Transformer Support
When configured in unocss.config.ts, UnoCSS transformers run in the loader and rewrite the module source (so the runtime markup matches the generated selectors), then tokens are extracted from the transformed code:
import { defineConfig, transformerVariantGroup } from "unocss";
import presetWind4 from "@unocss/preset-wind4";
export default defineConfig({
presets: [presetWind4()],
transformers: [transformerVariantGroup()],
});Template usage:
<template>
<button class="hover:(text-red-500 underline)">Hover me</button>
</template>Filesystem Content
UnoCSS content.filesystem patterns are included during extraction:
import { defineConfig } from "unocss";
export default defineConfig({
content: {
filesystem: ["content/**/*.md"],
},
});Inline Content
UnoCSS content.inline entries are also extracted:
import { defineConfig } from "unocss";
export default defineConfig({
content: {
inline: [
'<div class="text-blue-500 font-bold"></div>',
async () => ({
code: '<div class="rounded-lg p-4"></div>',
id: "inline-fixture",
}),
],
},
});Benchmarking
This repository includes a benchmark workspace that compares this plugin with the official @unocss/webpack/rspack integration.
Rspack UnoCSS Plugin Benchmark
- Vue files: 20000
- Runs: 3
| plugin | avg | median | min | max |
|---|---|---|---|---|
| rspack-unocss-plugin | 25.016s | 24.164s | 23.989s | 26.893s |
| @unocss/webpack/rspack | 31.771s | 33.681s | 27.851s | 33.782s |
Average delta: -21.26% versus @unocss/webpack/rspack Median delta: -28.26% versus @unocss/webpack/rspack
pnpm benchmark:ciDefault benchmark settings:
- Generates 10,000 Vue files at runtime.
- Builds the local plugin project and official plugin project.
- Runs each build 3 times by default.
- Reports average, median, minimum, and maximum build time.
- Writes
Benchmark/results/benchmark-summary.mdandBenchmark/results/benchmark-results.json.
Use smaller values for local smoke tests:
BENCHMARK_VUE_COUNT=100 BENCHMARK_RUNS=1 pnpm benchmark:ciGitHub Actions support is provided in .github/workflows/benchmark.yml. The workflow generates fixtures in CI instead of committing large generated files to the repository.
Benchmark results should be interpreted as end-to-end Rspack build numbers. Vue loader, Rspack module graph creation, filesystem I/O, JavaScript minification, CSS processing, and UnoCSS generation all contribute to the total time. Small percentage differences are expected because the UnoCSS integration is only one part of the full build pipeline.
Development
Repository Layout
.
├── packages/rspack-unocss-plugin # Plugin source, package metadata, tests
├── example # Vue + Rspack example project
├── Benchmark # Local-vs-official benchmark projects and scripts
└── .github/workflows/benchmark.yml # CI benchmark workflowScripts
pnpm build
pnpm test
pnpm example:build
pnpm example:dev
pnpm benchmark:ciPackage-level scripts:
pnpm --filter rspack-unocss-plugin build
pnpm --filter rspack-unocss-plugin testRunning Tests
pnpm testThe current test suite covers virtual CSS ID matching, layer/hash placeholders, transformer source rewriting, the source filter, and per-module token extraction.
Design Notes
Native CSS Boundary
The recommended pipeline is:
source files -> UnoCSS generator -> virtual uno.css -> Rspack native CSS -> CSS assetUnoCSS is responsible for:
- Source scanning and token extraction.
- Presets, rules, shortcuts, variants, and layers.
- UnoCSS transformers.
- CSS generation.
Rspack is responsible for:
- Module graph and bundling.
- Native CSS parsing and asset emission.
- Optional CSS transformation/minification through Lightning CSS.
This separation avoids forcing UnoCSS through PostCSS or webpack compatibility when Rspack can process generated CSS natively.
Vue SFC Compatibility
The native integration deliberately avoids reading resources with query strings as files. The loader operates on the source Rspack passes in and skips sub-requests that carry a query, so Vue SFC style sub-requests (which are loader-generated module requests, not disk paths) never trigger ENOENT, while class tokens are still extracted from the actual .vue source module.
Parallel Loaders
Rspack lets individual loaders opt into worker-thread execution via the per-loader parallel option. Token extraction in this plugin relies on an in-process context shared between the loader and the plugin instance, which a worker thread cannot reach. The plugin therefore pins its own loader to the main thread (parallel: false) and warns loudly if it ever finds itself without that context.
This is scoped to the UnoCSS loader only. Other loaders in your config (for example builtin:swc-loader or rspack-vue-loader) can still set parallel: true independently; doing so does not affect UnoCSS extraction.
Performance Expectations
The plugin can reduce overhead caused by webpack compatibility behavior and provide a cleaner Rspack-native path. However, in full application builds the total runtime includes many shared costs outside UnoCSS. Benchmarks should therefore focus on stability, parity, and regression detection rather than assuming large end-to-end speedups in every project.
In watch mode the plugin caches the generated CSS keyed by the union of extracted tokens (and the active config), so a rebuild that does not change which classes are present reuses the previous result instead of regenerating. This helps incremental rebuilds where edits do not alter the token set; it has no effect on a single production build.
Roadmap
- Expand integration tests with real Rspack compilation fixtures.
- Add broader coverage for Vue scoped styles, content filesystem, inline content, and layer outputs.
- Explore resolver-hook based virtual module resolution as an alternative to replacement-plugin based request mapping.
Contributing
Contributions are welcome. A good contribution workflow is:
- Open an issue or discussion describing the Rspack, UnoCSS, or framework behavior you want to improve.
- Add or update a focused fixture, test, or example that reproduces the behavior.
- Keep changes aligned with the native Rspack design: virtual CSS, UnoCSS generator APIs, and Rspack native CSS processing.
- Run the package tests and relevant example or benchmark command before submitting a pull request.
Useful commands:
pnpm test
pnpm build
pnpm example:build
BENCHMARK_VUE_COUNT=100 BENCHMARK_RUNS=1 pnpm benchmark:ciLicense
MIT License. See LICENSE for details.