Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/vite/src/node/plugins/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import {
injectQuery,
joinUrlSegments,
normalizePath,
rawRE,
removeLeadingSlash,
removeUrlQuery,
urlRE,
withTrailingSlash,
} from '../utils'
import { DEFAULT_ASSETS_INLINE_LIMIT, FS_PREFIX } from '../constants'
Expand All @@ -32,10 +35,7 @@ import type { ModuleGraph } from '../server/moduleGraph'
// referenceId is base64url but replaces - with $
export const assetUrlRE = /__VITE_ASSET__([\w$]+)__(?:\$_(.*?)__)?/g

const rawRE = /(?:\?|&)raw(?:&|$)/
export const urlRE = /(\?|&)url(?:&|$)/
const jsSourceMapRE = /\.[cm]?js\.map$/
const unnededFinalQueryCharRE = /[?&]$/

const assetCache = new WeakMap<ResolvedConfig, Map<string, string>>()

Expand Down Expand Up @@ -191,11 +191,11 @@ export function assetPlugin(config: ResolvedConfig): Plugin {
)}`
}

if (!config.assetsInclude(cleanUrl(id)) && !urlRE.test(id)) {
if (!urlRE.test(id) && !config.assetsInclude(cleanUrl(id))) {
return
}

id = id.replace(urlRE, '$1').replace(unnededFinalQueryCharRE, '')
id = removeUrlQuery(id)
let url = await fileToUrl(id, config, this)

// Inherit HMR timestamp if this asset was invalidated
Expand Down
200 changes: 159 additions & 41 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ import type { RawSourceMap } from '@ampproject/remapping'
import { getCodeWithSourcemap, injectSourcesContent } from '../server/sourcemap'
import type { ModuleNode } from '../server/moduleGraph'
import type { ResolveFn, ViteDevServer } from '../'
import { resolveUserExternal, toOutputFilePathInCss } from '../build'
import {
createToImportMetaURLBasedRelativeRuntime,
resolveUserExternal,
toOutputFilePathInCss,
toOutputFilePathInJS,
} from '../build'
import {
CLIENT_PUBLIC_PATH,
CSS_LANGS_RE,
Expand All @@ -42,10 +47,12 @@ import {
asyncReplace,
cleanUrl,
combineSourcemaps,
createSerialPromiseQueue,
emptyCssComments,
generateCodeFrame,
getHash,
getPackageManagerCommand,
injectQuery,
isDataUrl,
isExternalUrl,
isObject,
Expand All @@ -54,10 +61,12 @@ import {
parseRequest,
processSrcSet,
removeDirectQuery,
removeUrlQuery,
requireResolveFromRootWithFallback,
slash,
stripBase,
stripBomTag,
urlRE,
} from '../utils'
import type { Logger } from '../logger'
import { addToHTMLProxyTransformResult } from './html'
Expand Down Expand Up @@ -167,6 +176,7 @@ const inlineRE = /[?&]inline\b/
const inlineCSSRE = /[?&]inline-css\b/
const styleAttrRE = /[?&]style-attr\b/
const functionCallRE = /^[A-Z_][\w-]*\(/i
const transformOnlyRE = /[?&]transform-only\b/
const nonEscapedDoubleQuoteRe = /(?<!\\)(")/g

const cssBundleName = 'style.css'
Expand Down Expand Up @@ -220,10 +230,13 @@ function encodePublicUrlsInCSS(config: ResolvedConfig) {
return config.command === 'build'
}

const cssUrlAssetRE = /__VITE_CSS_URL__([\da-f]+)__/g

/**
* Plugin applied before user plugins
*/
export function cssPlugin(config: ResolvedConfig): Plugin {
const isBuild = config.command === 'build'
let server: ViteDevServer
let moduleCache: Map<string, Record<string, string>>

Expand Down Expand Up @@ -253,6 +266,32 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
removedPureCssFilesCache.set(config, new Map<string, RenderedChunk>())
},

async load(id) {
if (!isCSSRequest(id)) return

if (urlRE.test(id)) {
if (isModuleCSSRequest(id)) {
throw new Error(
`?url is not supported with CSS modules. (tried to import ${JSON.stringify(
id,
)})`,
)
}

// *.css?url
// in dev, it's handled by assets plugin.
if (isBuild) {
id = injectQuery(removeUrlQuery(id), 'transform-only')
return (
`import ${JSON.stringify(id)};` +
`export default "__VITE_CSS_URL__${Buffer.from(id).toString(
'hex',
)}__"`
)
}
}
},

async transform(raw, id, options) {
if (
!isCSSRequest(id) ||
Expand Down Expand Up @@ -374,8 +413,9 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
export function cssPostPlugin(config: ResolvedConfig): Plugin {
// styles initialization in buildStart causes a styling loss in watch
const styles: Map<string, string> = new Map<string, string>()
// list of css emit tasks to guarantee the files are emitted in a deterministic order
let emitTasks: Promise<void>[] = []
// queue to emit css serially to guarantee the files are emitted in a deterministic order
let codeSplitEmitQueue = createSerialPromiseQueue<string>()
const urlEmitQueue = createSerialPromiseQueue<unknown>()
let pureCssChunks: Set<RenderedChunk>

// when there are multiple rollup outputs and extracting CSS, only emit once,
Expand Down Expand Up @@ -414,7 +454,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
pureCssChunks = new Set<RenderedChunk>()
hasEmitted = false
chunkCSSMap = new Map()
emitTasks = []
codeSplitEmitQueue = createSerialPromiseQueue()
},

async transform(css, id, options) {
Expand Down Expand Up @@ -530,10 +570,13 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
const ids = Object.keys(chunk.modules)
for (const id of ids) {
if (styles.has(id)) {
chunkCSS += styles.get(id)
// a css module contains JS, so it makes this not a pure css chunk
if (cssModuleRE.test(id)) {
isPureCssChunk = false
// ?transform-only is used for ?url and shouldn't be included in normal CSS chunks
if (!transformOnlyRE.test(id)) {
chunkCSS += styles.get(id)
// a css module contains JS, so it makes this not a pure css chunk
if (cssModuleRE.test(id)) {
isPureCssChunk = false
}
}
} else {
// if the module does not have a style, then it's not a pure css chunk.
Expand All @@ -543,10 +586,6 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
}
}

if (!chunkCSS) {
return null
}

const publicAssetUrlMap = publicAssetUrlCache.get(config)!

// resolve asset URL placeholders to their built file URLs
Expand Down Expand Up @@ -608,6 +647,98 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
)
}

let s: MagicString | undefined
const urlEmitTasks: Array<{
cssAssetName: string
originalFilename: string
content: string
start: number
end: number
}> = []

if (code.includes('__VITE_CSS_URL__')) {
let match: RegExpExecArray | null
cssUrlAssetRE.lastIndex = 0
while ((match = cssUrlAssetRE.exec(code))) {
const [full, idHex] = match
const id = Buffer.from(idHex, 'hex').toString()
const originalFilename = cleanUrl(id)
const cssAssetName = ensureFileExt(
path.basename(originalFilename),
'.css',
)
if (!styles.has(id)) {
throw new Error(
`css content for ${JSON.stringify(id)} was not found`,
)
}

let cssContent = styles.get(id)!

cssContent = resolveAssetUrlsInCss(cssContent, cssAssetName)

urlEmitTasks.push({
cssAssetName,
originalFilename,
content: cssContent,
start: match.index,
end: match.index + full.length,
})
}
}

// should await even if this chunk does not include __VITE_CSS_URL__
// so that code after this line runs in the same order
await urlEmitQueue.run(async () =>
Promise.all(
urlEmitTasks.map(async (info) => {
info.content = await finalizeCss(info.content, true, config)
}),
),
)
if (urlEmitTasks.length > 0) {
const toRelativeRuntime = createToImportMetaURLBasedRelativeRuntime(
opts.format,
config.isWorker,
)
s ||= new MagicString(code)

for (const {
cssAssetName,
originalFilename,
content,
start,
end,
} of urlEmitTasks) {
const referenceId = this.emitFile({
name: cssAssetName,
type: 'asset',
source: content,
})
generatedAssets
.get(config)!
.set(referenceId, { originalName: originalFilename })

const replacement = toOutputFilePathInJS(
this.getFileName(referenceId),
'asset',
chunk.fileName,
'js',
config,
toRelativeRuntime,
)
const replacementString =
typeof replacement === 'string'
? JSON.stringify(replacement).slice(1, -1)
: `"+${replacement.runtime}+"`
s.update(start, end, replacementString)
}
}

if (!chunkCSS && !s) {
return null
}

if (config.build.cssCodeSplit) {
if (opts.format === 'es' || opts.format === 'cjs') {
if (isPureCssChunk) {
Expand All @@ -633,22 +764,11 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {

chunkCSS = resolveAssetUrlsInCss(chunkCSS, cssAssetName)

const previousTask = emitTasks[emitTasks.length - 1]
// finalizeCss is async which makes `emitFile` non-deterministic, so
// we use a `.then` to wait for previous tasks before finishing this
const thisTask = finalizeCss(chunkCSS, true, config).then((css) => {
chunkCSS = css
// make sure the previous task is also finished, this works recursively
return previousTask
// wait for previous tasks as well
chunkCSS = await codeSplitEmitQueue.run(async () => {
return finalizeCss(chunkCSS, true, config)
})

// push this task so the next task can wait for this one
emitTasks.push(thisTask)
const emitTasksLength = emitTasks.length

// wait for this and previous tasks to finish
await thisTask

// emit corresponding css file
const referenceId = this.emitFile({
name: cssAssetName,
Expand All @@ -659,11 +779,6 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
.get(config)!
.set(referenceId, { originalName: originalFilename, isEntry })
chunk.viteMetadata!.importedCss.add(this.getFileName(referenceId))

if (emitTasksLength === emitTasks.length) {
// this is the last task, clear `emitTasks` to free up memory
emitTasks = []
}
} else if (!config.build.ssr) {
// legacy build and inline css

Expand Down Expand Up @@ -697,24 +812,27 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
const insertMark = "'use strict';"
injectionPoint = code.indexOf(insertMark) + insertMark.length
}
const s = new MagicString(code)
s ||= new MagicString(code)
s.appendRight(injectionPoint, injectCode)
if (config.build.sourcemap) {
// resolve public URL from CSS paths, we need to use absolute paths
return {
code: s.toString(),
map: s.generateMap({ hires: 'boundary' }),
}
} else {
return { code: s.toString() }
}
}
} else {
// resolve public URL from CSS paths, we need to use absolute paths
chunkCSS = resolveAssetUrlsInCss(chunkCSS, cssBundleName)
// finalizeCss is called for the aggregated chunk in generateBundle

chunkCSSMap.set(chunk.fileName, chunkCSS)
}

if (s) {
if (config.build.sourcemap) {
return {
code: s.toString(),
map: s.generateMap({ hires: 'boundary' }),
}
} else {
return { code: s.toString() }
}
}
return null
},

Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/importAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
timeFrom,
transformStableResult,
unwrapId,
urlRE,
withTrailingSlash,
wrapId,
} from '../utils'
Expand All @@ -58,7 +59,6 @@ import type { ResolvedConfig } from '../config'
import type { Plugin } from '../plugin'
import { shouldExternalizeForSSR } from '../ssr/ssrExternal'
import { getDepsOptimizer, optimizedDepNeedsInterop } from '../optimizer'
import { urlRE } from './asset'
import { throwOutdatedRequest } from './optimizedDeps'
import { isCSSRequest, isDirectCSSRequest } from './css'
import { browserExternalId } from './resolve'
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/server/middlewares/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
removeImportQuery,
removeTimestampQuery,
unwrapId,
urlRE,
withTrailingSlash,
} from '../../utils'
import { send } from '../send'
Expand All @@ -38,7 +39,6 @@ import {
} from '../../plugins/optimizedDeps'
import { ERR_CLOSED_SERVER } from '../pluginContainer'
import { getDepsOptimizer } from '../../optimizer'
import { urlRE } from '../../plugins/asset'

const debugCache = createDebugger('vite:cache')

Expand Down
Loading