diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index d189ae06231d42..883279b56202de 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -172,6 +172,8 @@ const postcssConfigCache = new WeakMap< PostCSSConfigResult | null | Promise >() +const rootCssVariableCache = new WeakMap>() + function encodePublicUrlsInCSS(config: ResolvedConfig) { return config.command === 'build' } @@ -182,6 +184,7 @@ function encodePublicUrlsInCSS(config: ResolvedConfig) { export function cssPlugin(config: ResolvedConfig): Plugin { let server: ViteDevServer let moduleCache: Map> + let rootVarsCache: Map const resolveUrl = config.createResolver({ preferRelative: true, @@ -204,6 +207,9 @@ export function cssPlugin(config: ResolvedConfig): Plugin { moduleCache = new Map>() cssModulesCache.set(config, moduleCache) + rootVarsCache = new Map() + rootCssVariableCache.set(config, rootVarsCache) + removedPureCssFilesCache.set(config, new Map()) }, @@ -243,7 +249,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { modules, deps, map, - } = await compileCSS(id, raw, config, urlReplacer) + } = await compileCSS(id, raw, config, { urlReplacer, rootVarsCache }) if (modules) { moduleCache.set(id, modules) } @@ -491,6 +497,18 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { return null } + // remove duplicate base64 css variables + const base64VarRE = /(--base64-[^:]+):.+?\);/g + const cssVarCache = new Map() + chunkCSS = chunkCSS.replace(base64VarRE, (s, cssVar) => { + if (cssVarCache.has(cssVar)) { + return '' + } + + cssVarCache.set(cssVar, true) + return s + }) + const publicAssetUrlMap = publicAssetUrlCache.get(config)! // resolve asset URL placeholders to their built file URLs @@ -809,7 +827,10 @@ async function compileCSS( id: string, code: string, config: ResolvedConfig, - urlReplacer?: CssUrlReplacer, + options?: { + urlReplacer?: CssUrlReplacer + rootVarsCache: Map + }, ): Promise<{ code: string map?: SourceMapInput @@ -938,11 +959,12 @@ async function compileCSS( ) } - if (urlReplacer) { + if (options?.urlReplacer) { postcssPlugins.push( UrlRewritePostcssPlugin({ - replacer: urlReplacer, + replacer: options.urlReplacer, logger: config.logger, + rootVars: options.rootVarsCache, }), ) } @@ -1236,6 +1258,7 @@ const cssImageSetRE = /(?<=image-set\()((?:[\w\-]{1,256}\([^)]*\)|[^)])*)(?=\))/ const UrlRewritePostcssPlugin: PostCSS.PluginCreator<{ replacer: CssUrlReplacer logger: Logger + rootVars: Map }> = (opts) => { if (!opts) { throw new Error('base or replace is required') @@ -1245,6 +1268,7 @@ const UrlRewritePostcssPlugin: PostCSS.PluginCreator<{ postcssPlugin: 'vite-url-rewrite', Once(root) { const promises: Promise[] = [] + const isCssFile = !root.source?.input.file?.includes('.html') root.walkDecls((declaration) => { const importer = declaration.source?.input.file if (!importer) { @@ -1267,14 +1291,40 @@ const UrlRewritePostcssPlugin: PostCSS.PluginCreator<{ promises.push( rewriterToUse(declaration.value, replacerForDeclaration).then( (url) => { - declaration.value = url + let match + + if ( + isCssFile && + (match = url.match(/url\(['"]?data:.+?;base64,.+?\)/)) + ) { + const [base64] = match + + const cssVar = + opts.rootVars.get(base64) || `--base64-${getHash(base64)}` + opts.rootVars.set(base64, cssVar) + + declaration.value = url.replace(base64, `var(${cssVar})`) + } else { + declaration.value = url + } }, ), ) } }) if (promises.length) { - return Promise.all(promises) as any + return Promise.all(promises).then(() => { + isCssFile && + root.prepend( + `:root{${Array.from(opts.rootVars.entries()).reduce( + (cssVars, [url, cssVar]) => { + cssVars += `\n${cssVar}: ${url};` + return cssVars + }, + '', + )}}`, + ) + }) as any } }, }