diff --git a/packages/playground/css-codesplit/__tests__/css-codesplit.spec.ts b/packages/playground/css-codesplit/__tests__/css-codesplit.spec.ts index 95fe97a1b953ba..789adba23ae021 100644 --- a/packages/playground/css-codesplit/__tests__/css-codesplit.spec.ts +++ b/packages/playground/css-codesplit/__tests__/css-codesplit.spec.ts @@ -1,8 +1,23 @@ import { findAssetFile, getColor, isBuild, readManifest } from '../../testUtils' -test('should load both stylesheets', async () => { +test('should load all stylesheets', async () => { expect(await getColor('h1')).toBe('red') expect(await getColor('h2')).toBe('blue') + expect(await getColor('.dynamic')).toBe('green') +}) + +test('should load dynamic import with inline', async () => { + const css = await page.textContent('.dynamic-inline') + expect(css).toMatch('.inline') + + expect(await getColor('.inline')).not.toBe('yellow') +}) + +test('should load dynamic import with module', async () => { + const css = await page.textContent('.dynamic-module') + expect(css).toMatch('_mod_') + + expect(await getColor('.mod')).toBe('yellow') }) if (isBuild) { @@ -10,6 +25,7 @@ if (isBuild) { expect(findAssetFile(/style.*\.js$/)).toBe('') expect(findAssetFile('main.*.js$')).toMatch(`/* empty css`) expect(findAssetFile('other.*.js$')).toMatch(`/* empty css`) + expect(findAssetFile(/async.*\.js$/)).toBe('') }) test('should generate correct manifest', async () => { diff --git a/packages/playground/css-codesplit/async.css b/packages/playground/css-codesplit/async.css new file mode 100644 index 00000000000000..4902b2e7bee811 --- /dev/null +++ b/packages/playground/css-codesplit/async.css @@ -0,0 +1,3 @@ +.dynamic { + color: green; +} diff --git a/packages/playground/css-codesplit/index.html b/packages/playground/css-codesplit/index.html index 6b7b3bb2b4dc2d..63bdb59e11dc6b 100644 --- a/packages/playground/css-codesplit/index.html +++ b/packages/playground/css-codesplit/index.html @@ -1,2 +1,11 @@ +

This should be red

+

This should be blue

+ +

This should be green

+

This should not be yellow

+

+

This should be yellow

+

+
diff --git a/packages/playground/css-codesplit/inline.css b/packages/playground/css-codesplit/inline.css new file mode 100644 index 00000000000000..b2a2b5f1ead51f --- /dev/null +++ b/packages/playground/css-codesplit/inline.css @@ -0,0 +1,3 @@ +.inline { + color: yellow; +} diff --git a/packages/playground/css-codesplit/main.js b/packages/playground/css-codesplit/main.js index 8c80df2c181511..eb6e703f79e718 100644 --- a/packages/playground/css-codesplit/main.js +++ b/packages/playground/css-codesplit/main.js @@ -1,6 +1,15 @@ import './style.css' import './main.css' -document.getElementById( - 'app' -).innerHTML = `

This should be red

This should be blue

` +import('./async.css') + +import('./inline.css?inline').then((css) => { + document.querySelector('.dynamic-inline').textContent = css.default +}) + +import('./mod.module.css').then((css) => { + document.querySelector('.dynamic-module').textContent = JSON.stringify( + css.default + ) + document.querySelector('.mod').classList.add(css.default.mod) +}) diff --git a/packages/playground/css-codesplit/mod.module.css b/packages/playground/css-codesplit/mod.module.css new file mode 100644 index 00000000000000..7f84410485a32c --- /dev/null +++ b/packages/playground/css-codesplit/mod.module.css @@ -0,0 +1,3 @@ +.mod { + color: yellow; +} diff --git a/packages/playground/css/__tests__/css.spec.ts b/packages/playground/css/__tests__/css.spec.ts index e9372f67220c09..e650670b285b94 100644 --- a/packages/playground/css/__tests__/css.spec.ts +++ b/packages/playground/css/__tests__/css.spec.ts @@ -342,6 +342,12 @@ test('PostCSS dir-dependency', async () => { } }) +// skip because #8471 is reverted +test.skip('import dependency includes css import', async () => { + expect(await getColor('.css-js-dep')).toBe('green') + expect(await getColor('.css-js-dep-module')).toBe('green') +}) + test('URL separation', async () => { const urlSeparated = await page.$('.url-separated') const baseUrl = 'url(images/dog.webp)' @@ -417,5 +423,16 @@ 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' + ) +}) + +// skip because #8471 is reverted +test.skip('aliased css has content', async () => { + expect(await getColor('.aliased')).toBe('blue') + expect(await page.textContent('.aliased-content')).toMatch('.aliased') + expect(await getColor('.aliased-module')).toBe('blue') }) diff --git a/packages/playground/css/aliased/bar.module.css b/packages/playground/css/aliased/bar.module.css new file mode 100644 index 00000000000000..e4e46f3306a02e --- /dev/null +++ b/packages/playground/css/aliased/bar.module.css @@ -0,0 +1,3 @@ +.aliasedModule { + color: blue; +} diff --git a/packages/playground/css/aliased/foo.css b/packages/playground/css/aliased/foo.css new file mode 100644 index 00000000000000..7e32cb71a8f375 --- /dev/null +++ b/packages/playground/css/aliased/foo.css @@ -0,0 +1,3 @@ +.aliased { + color: blue; +} diff --git a/packages/playground/css/css-js-dep/bar.module.css b/packages/playground/css/css-js-dep/bar.module.css new file mode 100644 index 00000000000000..9d62f66761fa3d --- /dev/null +++ b/packages/playground/css/css-js-dep/bar.module.css @@ -0,0 +1,3 @@ +.cssJsDepModule { + color: green; +} diff --git a/packages/playground/css/css-js-dep/foo.css b/packages/playground/css/css-js-dep/foo.css new file mode 100644 index 00000000000000..515ee7693bff3f --- /dev/null +++ b/packages/playground/css/css-js-dep/foo.css @@ -0,0 +1,3 @@ +.css-js-dep { + color: green; +} diff --git a/packages/playground/css/css-js-dep/index.js b/packages/playground/css/css-js-dep/index.js new file mode 100644 index 00000000000000..853094b806fa97 --- /dev/null +++ b/packages/playground/css/css-js-dep/index.js @@ -0,0 +1,4 @@ +import './foo.css' +import barModuleClasses from './bar.module.css' + +export { barModuleClasses } diff --git a/packages/playground/css/css-js-dep/package.json b/packages/playground/css/css-js-dep/package.json new file mode 100644 index 00000000000000..d762f6566fdbdc --- /dev/null +++ b/packages/playground/css/css-js-dep/package.json @@ -0,0 +1,7 @@ +{ + "name": "css-js-dep", + "private": true, + "type": "module", + "version": "1.0.0", + "main": "index.js" +} diff --git a/packages/playground/css/index.html b/packages/playground/css/index.html index 15e81192cec7f1..0f30526cb31900 100644 --- a/packages/playground/css/index.html +++ b/packages/playground/css/index.html @@ -114,6 +114,13 @@

CSS

PostCSS dir-dependency (file 2): this should be grey too

+

+ import dependency includes 'import "./foo.css"': this should be green +

+

+ import dependency includes 'import "./bar.module.css"': this should be green +

+

URL separation preservation: should have valid background-image

@@ -138,6 +145,11 @@

CSS

PostCSS source.input.from. Should include query


+
+  

Aliased

+

import '#alias': this should be blue

+

+  

import '#alias-module': this should be blue

diff --git a/packages/playground/css/main.js b/packages/playground/css/main.js index f728b0251066d1..20ef6ed60fb51c 100644 --- a/packages/playground/css/main.js +++ b/packages/playground/css/main.js @@ -47,6 +47,11 @@ text('.charset-css', charset) import './dep.css' import './glob-dep.css' +import { barModuleClasses } from 'css-js-dep' +document + .querySelector('.css-js-dep-module') + .classList.add(barModuleClasses.cssJsDepModule) + function text(el, text) { document.querySelector(el).textContent = text } @@ -90,3 +95,10 @@ text('.imported-css-globEager', JSON.stringify(globEager, null, 2)) import postcssSourceInput from './postcss-source-input.css?query=foo' text('.postcss-source-input', postcssSourceInput) + +import aliasContent from '#alias' +text('.aliased-content', aliasContent) +import aliasModule from '#alias-module' +document + .querySelector('.aliased-module') + .classList.add(aliasModule.aliasedModule) diff --git a/packages/playground/css/package.json b/packages/playground/css/package.json index b45063100be089..d59d9108eb91c8 100644 --- a/packages/playground/css/package.json +++ b/packages/playground/css/package.json @@ -6,7 +6,11 @@ "dev": "vite", "build": "vite build", "debug": "node --inspect-brk ../../vite/bin/vite", - "preview": "vite preview" + "preview": "vite preview", + "postinstall": "ts-node ../../../scripts/patchFileDeps.ts" + }, + "dependencies": { + "css-js-dep": "file:./css-js-dep" }, "devDependencies": { "css-dep": "link:./css-dep", diff --git a/packages/playground/css/vite.config.js b/packages/playground/css/vite.config.js index 639a1302debb88..c501213b47cded 100644 --- a/packages/playground/css/vite.config.js +++ b/packages/playground/css/vite.config.js @@ -10,7 +10,9 @@ module.exports = { resolve: { alias: { '@': __dirname, - spacefolder: __dirname + '/folder with space' + spacefolder: __dirname + '/folder with space', + '#alias': __dirname + '/aliased/foo.css', + '#alias-module': __dirname + '/aliased/bar.module.css' } }, css: { diff --git a/packages/vite/src/node/importGlob.ts b/packages/vite/src/node/importGlob.ts index 0af49b8dc51268..70ad62321236bb 100644 --- a/packages/vite/src/node/importGlob.ts +++ b/packages/vite/src/node/importGlob.ts @@ -9,6 +9,7 @@ import { preloadMarker, preloadMethod } from './plugins/importAnalysisBuild' +import { isCSSRequest } from './plugins/css' import { blankReplacer, cleanUrl, @@ -149,14 +150,16 @@ export async function transformImportGlob( await fsp.readFile(path.join(base, files[i]), 'utf-8') )},` } else { + const importeeUrl = isCSSRequest(importee) ? `${importee}?used` : importee if (isEager) { const identifier = `__glob_${importIndex}_${i}` + // css imports injecting a ?used query to export the css string importsString += `import ${ isEagerDefault ? `` : `* as ` - }${identifier} from ${JSON.stringify(importee)};` + }${identifier} from ${JSON.stringify(importeeUrl)};` entries += ` ${JSON.stringify(file)}: ${identifier},` } else { - let imp = `import(${JSON.stringify(importee)})` + let imp = `import(${JSON.stringify(importeeUrl)})` if (!normalizeUrl && preload) { imp = `(${isModernFlag}` + diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 87add94d261845..e3532cec97cbf6 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -102,6 +102,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 enum PreprocessLang { @@ -369,19 +370,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 dbe2ffd79770bf..e3b42179fd6314 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -5,11 +5,11 @@ import { init, parse as parseImports } from 'es-module-lexer' import type { OutputChunk, SourceMap } from 'rollup' import type { RawSourceMap } from '@ampproject/remapping' import { transformImportGlob } from '../importGlob' -import { combineSourcemaps } from '../utils' +import { bareImportRE, combineSourcemaps } from '../utils' import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' import { genSourceMapUrl } from '../server/sourcemap' -import { removedPureCssFilesCache } from './css' +import { isCSSRequest, removedPureCssFilesCache } from './css' /** * A flag for injected helpers. This flag will be set to `false` if the output @@ -148,9 +148,12 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { e: end, ss: expStart, se: expEnd, + n: specifier, d: dynamicIndex } = imports[index] + const isDynamic = dynamicIndex > -1 + // import.meta.glob if ( source.slice(start, end) === 'import.meta' && @@ -188,11 +191,32 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { continue } - if (dynamicIndex > -1 && insertPreload) { + if (isDynamic && insertPreload) { needPreloadHelper = true - const original = source.slice(expStart, expEnd) - const replacement = `${preloadMethod}(() => ${original},${isModernFlag}?"${preloadMarker}":void 0)` - str().overwrite(expStart, expEnd, replacement, { contentOnly: true }) + str().prependLeft(expStart, `${preloadMethod}(() => `) + str().appendRight( + expEnd, + `,${isModernFlag}?"${preloadMarker}":void 0)` + ) + } + + // 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') || isDynamic) && + // 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, isDynamic ? `'${url}'` : url, { + contentOnly: true + }) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 609e5f1870459e..48d3cf3b4b6a5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,11 +137,14 @@ importers: packages/playground/css: specifiers: css-dep: link:./css-dep + css-js-dep: file:./css-js-dep fast-glob: ^3.2.11 less: ^4.1.2 postcss-nested: ^5.0.6 sass: ^1.43.4 stylus: ^0.55.0 + dependencies: + css-js-dep: link:css-js-dep devDependencies: css-dep: link:css-dep fast-glob: 3.2.11 @@ -171,6 +174,9 @@ importers: packages/playground/css/css-dep: specifiers: {} + packages/playground/css/css-js-dep: + specifiers: {} + packages/playground/css/pkg-dep: specifiers: {} @@ -5836,7 +5842,7 @@ packages: dev: true /image-size/0.5.5: - resolution: {integrity: sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=} + resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} engines: {node: '>=0.10.0'} hasBin: true requiresBuild: true