diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index b15eed055fd726..ef0f7404763f73 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -111,6 +111,7 @@ const htmlProxyRE = /(\?|&)html-proxy\b/ const commonjsProxyRE = /\?commonjs-proxy/ const inlineRE = /(\?|&)inline\b/ const inlineCSSRE = /(\?|&)inline-css\b/ +const usedRE = /(\?|&)used\b/ const varRE = /^var\(/i const cssBundleName = 'style.css' @@ -414,19 +415,18 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { } let code: string - if (modulesCode) { - code = modulesCode - } else { - let content = css - if (config.build.minify) { - content = await minifyCSS(content, config) + if (usedRE.test(id)) { + if (modulesCode) { + code = modulesCode + } else { + let content = css + if (config.build.minify) { + content = await minifyCSS(content, config) + } + code = `export default ${JSON.stringify(content)}` } - // marking as pure to make it tree-shakable by minifier - // but the module itself is still treated as a non tree-shakable module - // because moduleSideEffects is 'no-treeshake' - code = `export default /* #__PURE__ */ (() => ${JSON.stringify( - content - )})()` + } else { + code = `export default ''` } return { diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 613d7ca5a08503..b83ae46451c8f7 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -6,6 +6,7 @@ import type { OutputChunk, SourceMap } from 'rollup' import colors from 'picocolors' import type { RawSourceMap } from '@ampproject/remapping' import { + bareImportRE, cleanUrl, combineSourcemaps, isDataUrl, @@ -16,7 +17,7 @@ import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' import { genSourceMapUrl } from '../server/sourcemap' import { getDepsOptimizer, optimizedDepNeedsInterop } from '../optimizer' -import { removedPureCssFilesCache } from './css' +import { isCSSRequest, removedPureCssFilesCache } from './css' import { interopNamedImports } from './importAnalysis' /** @@ -238,13 +239,9 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { ) } - if (!depsOptimizer) { - continue - } - // static import or valid string in dynamic import // If resolvable, let's resolve it - if (specifier) { + if (depsOptimizer && specifier) { // skip external / data uri if (isExternalUrl(specifier) || isDataUrl(specifier)) { continue @@ -297,6 +294,27 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { } } } + + // Differentiate CSS imports that use the default export from those that + // do not by injecting a ?used query - this allows us to avoid including + // the CSS string when unnecessary (esbuild has trouble tree-shaking + // them) + if ( + specifier && + isCSSRequest(specifier) && + // always inject ?used query when it is a dynamic import + // because there is no way to check whether the default export is used + (source.slice(expStart, start).includes('from') || isDynamicImport) && + // already has ?used query (by import.meta.glob) + !specifier.match(/\?used(&|$)/) && + // edge case for package names ending with .css (e.g normalize.css) + !(bareImportRE.test(specifier) && !specifier.includes('/')) + ) { + const url = specifier.replace(/\?|$/, (m) => `?used${m ? '&' : ''}`) + str().overwrite(start, end, isDynamicImport ? `'${url}'` : url, { + contentOnly: true + }) + } } if ( diff --git a/packages/vite/src/node/plugins/importMetaGlob.ts b/packages/vite/src/node/plugins/importMetaGlob.ts index 7f3695178fdce9..5e0f473317b798 100644 --- a/packages/vite/src/node/plugins/importMetaGlob.ts +++ b/packages/vite/src/node/plugins/importMetaGlob.ts @@ -18,6 +18,7 @@ import type { ViteDevServer } from '../server' import type { ModuleNode } from '../server/moduleGraph' import type { ResolvedConfig } from '../config' import { normalizePath, slash, transformStableResult } from '../utils' +import { isCSSRequest } from './css' const { isMatch, scan } = micromatch @@ -392,6 +393,9 @@ export async function transformGlobImport( let importPath = paths.importPath let importQuery = query + if (isCSSRequest(file)) + importQuery = importQuery ? `${importQuery}&used` : '?used' + if (importQuery && importQuery !== '?raw') { const fileExtension = basename(file).split('.').slice(-1)[0] if (fileExtension && restoreQueryExtension) diff --git a/playground/css/__tests__/css.spec.ts b/playground/css/__tests__/css.spec.ts index 3c9f5fc4ea9b0a..b5af00d1a4f537 100644 --- a/playground/css/__tests__/css.spec.ts +++ b/playground/css/__tests__/css.spec.ts @@ -429,7 +429,11 @@ test("relative path rewritten in Less's data-uri", async () => { test('PostCSS source.input.from includes query', async () => { const code = await page.textContent('.postcss-source-input') // should resolve assets - expect(code).toContain('/postcss-source-input.css?query=foo') + expect(code).toContain( + isBuild + ? '/postcss-source-input.css?used&query=foo' + : '/postcss-source-input.css?query=foo' + ) }) test('aliased css has content', async () => {