@aemvite/vite-plugin-aem-css-url-passthrough
v0.7.0
Published
Vite plugin that rewrites CSS url() references in built clientlib stylesheets back to ../resources/<sub>/<file> form, mirroring webpack css-loader { url: false } for AEM clientlibs.
Maintainers
Readme
@aemvite/vite-plugin-aem-css-url-passthrough
Vite plugin that rewrites CSS
url(...)references in built AEM clientlib stylesheets back to the canonical../resources/<sub>/<file>form. Drop-in replacement for webpack'scss-loader: { url: false }in AEM clientlib builds.
Part of the @aemvite/* toolchain.
Install
npm i -D @aemvite/vite-plugin-aem-css-url-passthrough- Peer dependency:
vite^8 - Engines: Node
^20.19.0 || ^22.18.0 || >=24.11.0 - No runtime dependencies — uses only Node's built-in
node:fsandnode:path.
What it does
AEM clientlibs serve CSS at <clientlib>/css/<name>.css and static assets at
<clientlib>/resources/<sub>/<file>. SCSS authors typically write
url("../resources/images/foo.svg") relative to the final clientlib layout.
The webpack stack preserved those literals via css-loader: { url: false },
but Vite/Rolldown by default rewrites url() relative to the source SCSS
file, producing paths like ../../components/header/resources/images/foo.svg
that 404 against the deployed clientlib.
This plugin runs at writeBundle, scans every emitted .css file in the
build output directory, and rewrites every url(...) whose body contains
resources/<configured-sub>/ to the canonical ../resources/<sub>/<rest>
form. Other URLs are left untouched.
writeBundle is used (not generateBundle) because Vite's lib-mode CSS
extraction emits the .css outside the Rollup chunk map.
Usage
Direct (any Vite project)
// vite.config.ts
import { defineConfig } from "vite";
import { aemCssUrlPassthrough } from "@aemvite/vite-plugin-aem-css-url-passthrough";
export default defineConfig({
plugins: [
aemCssUrlPassthrough({
// Optional; defaults to ["images", "fonts"].
resourceDirs: ["images", "fonts", "icons", "storybook-assets"],
}),
],
});Through @aemvite/aem-config (recommended)
Set the cssUrlPassthrough flag in your aem.config.{ts,mjs} and the plugin
is auto-wired into every clientlib build:
import { defineAemConfig } from "@aemvite/aem-config";
export default defineAemConfig({
clientLibRoot: "../ui.apps/.../clientlibs",
cssUrlPassthrough: true, // or: { resourceDirs: ["images", "fonts", "icons"] }
clientlibs: [
/* ... */
],
});The same field is available per-clientlib and takes precedence over the global value.
API reference
Plugin
| Export | Signature | Notes |
|---|---|---|
| aemCssUrlPassthrough | (options?: AemCssUrlPassthroughOptions) => Plugin | Vite plugin (also the package default export). apply: "build", runs at writeBundle. |
Types
interface AemCssUrlPassthroughOptions {
/** Default: ["images", "fonts"]. */
resourceDirs?: readonly string[];
}Behavior
- Only
.cssfiles in the build output directory are scanned. No URL rewriting touches the bundled JavaScript or any nested subdirectory. - Only
url(...)s whose body containsresources/<configured-sub>/are rewritten. The rewritten URL becomes../resources/<sub>/<rest>. - Data URIs, absolute
http(s)://URLs, and protocol-relative//hostURLs are skipped. External CDN references are never touched. - Idempotent. A url() already in canonical form (
url(../resources/...)) is matched and rewritten to the identical string. - Sync content only. The plugin runs once per build, after Rollup writes the bundle, and rewrites in place.
Notes & caveats
- The plugin only walks the top level of the build output directory. AEM
clientlib builds emit a single flat
<name>.cssper clientlib, so this is by design. - Configure every
resources/<sub>/bucket your stylesheets reference. If a url() goes through a directory you didn't list, it will be left as-is and will likely 404 against the AEM clientlib.
License
MIT © Luca Nerlich
Repository
https://github.com/LucaNerlich/aem-vite (this package lives in
packages/vite-plugin-aem-css-url-passthrough).
