diff --git a/packages/vite/src/node/constants.ts b/packages/vite/src/node/constants.ts index 83f97586e6ea6d..a812b04ab7a10f 100644 --- a/packages/vite/src/node/constants.ts +++ b/packages/vite/src/node/constants.ts @@ -50,6 +50,8 @@ export const OPTIMIZABLE_ENTRY_RE = /\.[cm]?[jt]s$/ export const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/ +export const URL_RE = /(\?|&)url(?:&|$)/ + /** * Prefix for resolved fs paths, since windows paths may not be valid as URLs. */ diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 1e8b6303488fc3..8ae2d2e2b9f443 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -18,6 +18,7 @@ import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' import { cleanUrl, getHash, joinUrlSegments, normalizePath } from '../utils' import { FS_PREFIX } from '../constants' +import { isCSSRequest } from './css' export const assetUrlRE = /__VITE_ASSET__([a-z\d]+)__(?:\$_(.*?)__)?/g @@ -151,6 +152,11 @@ export function assetPlugin(config: ResolvedConfig): Plugin { return } + if (isCSSRequest(id) && urlRE.test(id)) { + // leave css?url to css plugin + return + } + // raw requests, read from disk if (rawRE.test(id)) { const file = checkPublicFile(id, config) || cleanUrl(id) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 3be227204d9b40..0eceefd50fff5d 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -26,7 +26,7 @@ import { getCodeWithSourcemap, injectSourcesContent } from '../server/sourcemap' import type { ModuleNode } from '../server/moduleGraph' import type { ResolveFn, ViteDevServer } from '../' import { toOutputFilePathInCss } from '../build' -import { CLIENT_PUBLIC_PATH, SPECIAL_QUERY_RE } from '../constants' +import { CLIENT_PUBLIC_PATH, SPECIAL_QUERY_RE, URL_RE } from '../constants' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' import { @@ -114,6 +114,9 @@ const inlineCSSRE = /(?:\?|&)inline-css\b/ const usedRE = /(?:\?|&)used\b/ const varRE = /^var\(/i +const viteCSSURLMarker = '__VITE_CSS_URL_' +const viteCSSURLMarkerRE = /__VITE_CSS_URL_([^"]+)/ + const cssBundleName = 'style.css' const enum PreprocessLang { @@ -153,6 +156,21 @@ export const removedPureCssFilesCache = new WeakMap< Map >() +export const pureCssIdMapAssetUrl = new Map() +export const removedDynamicCssUrlsCache = new Map() + +export function getDynamicCssAssetUrl(importUrl: string): string | undefined { + const pureCssId = removedDynamicCssUrlsCache.get(importUrl) + return pureCssId ? pureCssIdMapAssetUrl.get(pureCssId) : undefined +} + +/** + * remove ?url from module id + */ +export function getPureCssId(id: string, root: string): string { + return path.relative(root, id.replace(/\?.+$/, '')) +} + const postcssConfigCache: Record< string, WeakMap @@ -182,6 +200,23 @@ export function cssPlugin(config: ResolvedConfig): Plugin { server = _server }, + load(id) { + if (isCSSRequest(id) && URL_RE.test(id)) { + const pureCssId = getPureCssId(id, config.root) + + if (config.command === 'serve') { + return `export default "${config.base}${pureCssId}"` + } + + this.emitFile({ + type: 'chunk', + id: pureCssId + }) + + return `export default "${viteCSSURLMarker}${pureCssId}"` + } + }, + buildStart() { // Ensure a new cache for every build (i.e. rebuilding in watch mode) moduleCache = new Map>() @@ -447,6 +482,16 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { }, async renderChunk(code, chunk, opts) { + if ( + chunk.facadeModuleId && + isCSSRequest(chunk.facadeModuleId) && + URL_RE.test(chunk.facadeModuleId) + ) { + // mark css url file + // connect fileName with pureCssId + const pureCssId = getPureCssId(chunk.facadeModuleId, config.root) + removedDynamicCssUrlsCache.set(chunk.fileName, pureCssId) + } let chunkCSS = '' let isPureCssChunk = true const ids = Object.keys(chunk.modules) @@ -557,7 +602,15 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { generatedAssets .get(config)! .set(referenceId, { originalName, isEntry }) - chunk.viteMetadata.importedCss.add(this.getFileName(referenceId)) + const fileName = this.getFileName(referenceId) + if (chunk.facadeModuleId) { + // connect css id with css file name + pureCssIdMapAssetUrl.set( + getPureCssId(chunk.facadeModuleId, config.root), + config.base + fileName + ) + } + chunk.viteMetadata.importedCss.add(fileName) } else if (!config.build.ssr) { // legacy build and inline css @@ -667,6 +720,11 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { // remove css import while preserving source map location (m) => `/* empty css ${''.padEnd(m.length - 15)}*/` ) + chunk.code = chunk.code.replace( + viteCSSURLMarkerRE, + // replace css module id with css file name + (_, matchId) => pureCssIdMapAssetUrl.get(matchId) || matchId + ) } } const removedPureCssFiles = removedPureCssFilesCache.get(config)! diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index c8be0da8a95b32..8e2ef5a6dc433d 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -19,7 +19,12 @@ import type { ResolvedConfig } from '../config' import { toOutputFilePathInJS } from '../build' import { genSourceMapUrl } from '../server/sourcemap' import { getDepsOptimizer, optimizedDepNeedsInterop } from '../optimizer' -import { isCSSRequest, removedPureCssFilesCache } from './css' +import { URL_RE } from '../constants' +import { + getDynamicCssAssetUrl, + isCSSRequest, + removedPureCssFilesCache +} from './css' import { interopNamedImports } from './importAnalysis' /** @@ -291,7 +296,11 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { str().remove(end + 1, expEnd) } - if (isDynamicImport && insertPreload) { + // css?url should not wrapped with preloadMethod + const isCssUrl = + !!specifier && isCSSRequest(specifier) && URL_RE.test(specifier) + + if (isDynamicImport && insertPreload && !isCssUrl) { needPreloadHelper = true str().prependLeft(expStart, `${preloadMethod}(() => `) str().appendRight( @@ -468,6 +477,16 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { url ) + // change dynamic import to local Promise + const cssAssetUrl = getDynamicCssAssetUrl(normalizedFile) + if (cssAssetUrl) { + s.update( + expStart, + expEnd, + `Promise.resolve({default:"${cssAssetUrl}"})` + ) + } + const ownerFilename = chunk.fileName // literal import - trace direct imports and add to deps const analyzed: Set = new Set()