diff --git a/packages/vite/src/node/__tests__/asset.spec.ts b/packages/vite/src/node/__tests__/asset.spec.ts deleted file mode 100644 index 8f627ce299e3ec..00000000000000 --- a/packages/vite/src/node/__tests__/asset.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { describe, expect, test } from 'vitest' -import { assetFileNamesToFileName } from '../plugins/asset' - -describe('assetFileNamesToFileName', () => { - // on Windows, both forward slashes and backslashes may appear in the input - const sourceFilepaths: readonly string[] = - process.platform === 'win32' - ? ['C:/path/to/source/input.png', 'C:\\path\\to\\source\\input.png'] - : ['/path/to/source/input.png'] - - for (const sourceFilepath of sourceFilepaths) { - const content = Buffer.alloc(0) - const contentHash = 'abcd1234' - - // basic examples - - test('a string with no placeholders', () => { - const fileName = assetFileNamesToFileName( - 'output.png', - sourceFilepath, - contentHash, - content - ) - - expect(fileName).toBe('output.png') - }) - - test('a string with placeholders', () => { - const fileName = assetFileNamesToFileName( - 'assets/[name]/[ext]/[extname]/[hash]', - sourceFilepath, - contentHash, - content - ) - - expect(fileName).toBe('assets/input/png/.png/abcd1234') - }) - - // function examples - - test('a function that uses asset information', () => { - const fileName = assetFileNamesToFileName( - (options) => - `assets/${options.name.replace(/^C:|[/\\]/g, '')}/${options.type}/${ - options.source.length - }`, - sourceFilepath, - contentHash, - content - ) - - expect(fileName).toBe('assets/pathtosourceinput.png/asset/0') - }) - - test('a function that returns a string with no placeholders', () => { - const fileName = assetFileNamesToFileName( - () => 'output.png', - sourceFilepath, - contentHash, - content - ) - - expect(fileName).toBe('output.png') - }) - - test('a function that returns a string with placeholders', () => { - const fileName = assetFileNamesToFileName( - () => 'assets/[name]/[ext]/[extname]/[hash]', - sourceFilepath, - contentHash, - content - ) - - expect(fileName).toBe('assets/input/png/.png/abcd1234') - }) - - // invalid cases - - test('a string with an invalid placeholder', () => { - expect(() => { - assetFileNamesToFileName( - 'assets/[invalid]', - sourceFilepath, - contentHash, - content - ) - }).toThrowError( - 'invalid placeholder [invalid] in assetFileNames "assets/[invalid]"' - ) - - expect(() => { - assetFileNamesToFileName( - 'assets/[name][invalid][extname]', - sourceFilepath, - contentHash, - content - ) - }).toThrowError( - 'invalid placeholder [invalid] in assetFileNames "assets/[name][invalid][extname]"' - ) - }) - - test('a function that returns a string with an invalid placeholder', () => { - expect(() => { - assetFileNamesToFileName( - () => 'assets/[invalid]', - sourceFilepath, - contentHash, - content - ) - }).toThrowError( - 'invalid placeholder [invalid] in assetFileNames "assets/[invalid]"' - ) - - expect(() => { - assetFileNamesToFileName( - () => 'assets/[name][invalid][extname]', - sourceFilepath, - contentHash, - content - ) - }).toThrowError( - 'invalid placeholder [invalid] in assetFileNames "assets/[name][invalid][extname]"' - ) - }) - - test('a number', () => { - expect(() => { - assetFileNamesToFileName( - 9876 as unknown as string, - sourceFilepath, - contentHash, - content - ) - }).toThrowError('assetFileNames must be a string or a function') - }) - - test('a function that returns a number', () => { - expect(() => { - assetFileNamesToFileName( - () => 9876 as unknown as string, - sourceFilepath, - contentHash, - content - ) - }).toThrowError('assetFileNames must return a string') - }) - } -}) diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index aa7832b14aa8e7..ac06868bebfead 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -5,9 +5,7 @@ import { Buffer } from 'node:buffer' import * as mrmime from 'mrmime' import type { NormalizedOutputOptions, - OutputOptions, PluginContext, - PreRenderedAsset, RenderedChunk } from 'rollup' import MagicString from 'magic-string' @@ -29,10 +27,15 @@ const urlRE = /(\?|&)url(?:&|$)/ const assetCache = new WeakMap>() // chunk.name is the basename for the asset ignoring the directory structure -// For the manifest, we need to preserve the original file path +// For the manifest, we need to preserve the original file path and isEntry +// for CSS assets. We keep a map from referenceId to this information. +export interface GeneratedAssetMeta { + originalName: string + isEntry?: boolean +} export const generatedAssets = new WeakMap< ResolvedConfig, - Map + Map >() // add own dictionary entry by directly assigning mrmime @@ -248,120 +251,6 @@ export function getPublicAssetFilename( return publicAssetUrlCache.get(config)?.get(hash) } -export function resolveAssetFileNames( - config: ResolvedConfig -): string | ((chunkInfo: PreRenderedAsset) => string) { - const output = config.build?.rollupOptions?.output - const defaultAssetFileNames = path.posix.join( - config.build.assetsDir, - '[name].[hash][extname]' - ) - // Steps to determine which assetFileNames will be actually used. - // First, if output is an object or string, use assetFileNames in it. - // And a default assetFileNames as fallback. - let assetFileNames: Exclude = - (output && !Array.isArray(output) ? output.assetFileNames : undefined) ?? - defaultAssetFileNames - if (output && Array.isArray(output)) { - // Second, if output is an array, adopt assetFileNames in the first object. - assetFileNames = output[0].assetFileNames ?? assetFileNames - } - return assetFileNames -} - -/** - * converts the source filepath of the asset to the output filename based on the assetFileNames option. \ - * this function imitates the behavior of rollup.js. \ - * https://rollupjs.org/guide/en/#outputassetfilenames - * - * @example - * ```ts - * const content = Buffer.from('text'); - * const fileName = assetFileNamesToFileName( - * 'assets/[name].[hash][extname]', - * '/path/to/file.txt', - * getHash(content), - * content - * ) - * // fileName: 'assets/file.982d9e3e.txt' - * ``` - * - * @param assetFileNames filename pattern. e.g. `'assets/[name].[hash][extname]'` - * @param file filepath of the asset - * @param contentHash hash of the asset. used for `'[hash]'` placeholder - * @param content content of the asset. passed to `assetFileNames` if `assetFileNames` is a function - * @returns output filename - */ -export function assetFileNamesToFileName( - assetFileNames: Exclude, - file: string, - contentHash: string, - content: string | Buffer -): string { - const basename = path.basename(file) - - // placeholders for `assetFileNames` - // `hash` is slightly different from the rollup's one - const extname = path.extname(basename) - const ext = extname.substring(1) - const name = basename.slice(0, -extname.length) - const hash = contentHash - - if (typeof assetFileNames === 'function') { - assetFileNames = assetFileNames({ - name: file, - source: content, - type: 'asset' - }) - if (typeof assetFileNames !== 'string') { - throw new TypeError('assetFileNames must return a string') - } - } else if (typeof assetFileNames !== 'string') { - throw new TypeError('assetFileNames must be a string or a function') - } - - const fileName = assetFileNames.replace( - /\[\w+\]/g, - (placeholder: string): string => { - switch (placeholder) { - case '[ext]': - return ext - - case '[extname]': - return extname - - case '[hash]': - return hash - - case '[name]': - return sanitizeFileName(name) - } - throw new Error( - `invalid placeholder ${placeholder} in assetFileNames "${assetFileNames}"` - ) - } - ) - - return fileName -} - -// taken from https://github.com/rollup/rollup/blob/a8647dac0fe46c86183be8596ef7de25bc5b4e4b/src/utils/sanitizeFileName.ts -// https://datatracker.ietf.org/doc/html/rfc2396 -// eslint-disable-next-line no-control-regex -const INVALID_CHAR_REGEX = /[\x00-\x1F\x7F<>*#"{}|^[\]`;?:&=+$,]/g -const DRIVE_LETTER_REGEX = /^[a-z]:/i -function sanitizeFileName(name: string): string { - const match = DRIVE_LETTER_REGEX.exec(name) - const driveLetter = match ? match[0] : '' - - // A `:` is only allowed as part of a windows drive letter (ex: C:\foo) - // Otherwise, avoid them because they can refer to NTFS alternate data streams. - return ( - driveLetter + - name.substr(driveLetter.length).replace(INVALID_CHAR_REGEX, '_') - ) -} - export const publicAssetUrlCache = new WeakMap< ResolvedConfig, // hash -> url diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index fb23eb0fd6953e..01875173151fa2 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -52,15 +52,14 @@ import { import type { Logger } from '../logger' import { addToHTMLProxyTransformResult } from './html' import { - assetFileNamesToFileName, assetUrlRE, checkPublicFile, fileToUrl, + generatedAssets, publicAssetUrlCache, publicAssetUrlRE, publicFileToBuiltUrl, - renderAssetUrlInJS, - resolveAssetFileNames + renderAssetUrlInJS } from './asset' import type { ESBuildOptions } from './esbuild' @@ -153,8 +152,6 @@ export const removedPureCssFilesCache = new WeakMap< Map >() -export const cssEntryFilesCache = new WeakMap>() - const postcssConfigCache: Record< string, WeakMap @@ -190,7 +187,6 @@ export function cssPlugin(config: ResolvedConfig): Plugin { cssModulesCache.set(config, moduleCache) removedPureCssFilesCache.set(config, new Map()) - cssEntryFilesCache.set(config, new Set()) }, async transform(raw, id, options) { @@ -470,7 +466,6 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { return null } - const cssEntryFiles = cssEntryFilesCache.get(config)! const publicAssetUrlMap = publicAssetUrlCache.get(config)! // resolve asset URL placeholders to their built file URLs @@ -547,24 +542,21 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { const lang = path.extname(cssAssetName).slice(1) const cssFileName = ensureFileExt(cssAssetName, '.css') - if (chunk.isEntry && isPureCssChunk) cssEntryFiles.add(cssAssetName) - chunkCSS = resolveAssetUrlsInCss(chunkCSS, cssAssetName) chunkCSS = await finalizeCss(chunkCSS, true, config) // emit corresponding css file - const fileHandle = this.emitFile({ - name: isPreProcessor(lang) ? cssAssetName : cssFileName, - fileName: assetFileNamesToFileName( - resolveAssetFileNames(config), - cssFileName, - getHash(chunkCSS), - chunkCSS - ), + const referenceId = this.emitFile({ + name: path.basename(cssFileName), type: 'asset', source: chunkCSS }) - chunk.viteMetadata.importedCss.add(this.getFileName(fileHandle)) + const originalName = isPreProcessor(lang) ? cssAssetName : cssFileName + const isEntry = chunk.isEntry && isPureCssChunk + generatedAssets + .get(config)! + .set(referenceId, { originalName, isEntry }) + chunk.viteMetadata.importedCss.add(this.getFileName(referenceId)) } else if (!config.build.ssr) { // legacy build and inline css diff --git a/packages/vite/src/node/plugins/manifest.ts b/packages/vite/src/node/plugins/manifest.ts index 45b9f703ece106..59da888002ff30 100644 --- a/packages/vite/src/node/plugins/manifest.ts +++ b/packages/vite/src/node/plugins/manifest.ts @@ -3,8 +3,8 @@ import type { OutputAsset, OutputChunk } from 'rollup' import type { ResolvedConfig } from '..' import type { Plugin } from '../plugin' import { normalizePath } from '../utils' -import { cssEntryFilesCache } from './css' import { generatedAssets } from './asset' +import type { GeneratedAssetMeta } from './asset' export type Manifest = Record @@ -101,21 +101,22 @@ export function manifestPlugin(config: ResolvedConfig): Plugin { return manifestChunk } - function createAsset(chunk: OutputAsset, src: string): ManifestChunk { + function createAsset( + asset: OutputAsset, + src: string, + isEntry?: boolean + ): ManifestChunk { const manifestChunk: ManifestChunk = { - file: chunk.fileName, + file: asset.fileName, src } - - if (cssEntryFiles.has(chunk.name!)) manifestChunk.isEntry = true - + if (isEntry) manifestChunk.isEntry = true return manifestChunk } - const cssEntryFiles = cssEntryFilesCache.get(config)! - - const fileNameToAssetMeta = new Map() - generatedAssets.get(config)!.forEach((asset, referenceId) => { + const fileNameToAssetMeta = new Map() + const assets = generatedAssets.get(config)! + assets.forEach((asset, referenceId) => { const fileName = this.getFileName(referenceId) fileNameToAssetMeta.set(fileName, asset) }) @@ -128,17 +129,18 @@ export function manifestPlugin(config: ResolvedConfig): Plugin { manifest[getChunkName(chunk)] = createChunk(chunk) } else if (chunk.type === 'asset' && typeof chunk.name === 'string') { // Add every unique asset to the manifest, keyed by its original name - const src = - fileNameToAssetMeta.get(chunk.fileName)?.originalName ?? chunk.name - const asset = createAsset(chunk, src) + const assetMeta = fileNameToAssetMeta.get(chunk.fileName) + const src = assetMeta?.originalName ?? chunk.name + const asset = createAsset(chunk, src, assetMeta?.isEntry) manifest[src] = asset fileNameToAsset.set(chunk.fileName, asset) } } - // Add duplicate assets to the manifest - fileNameToAssetMeta.forEach(({ originalName }, fileName) => { + // Add deduplicated assets to the manifest + assets.forEach(({ originalName }, referenceId) => { if (!manifest[originalName]) { + const fileName = this.getFileName(referenceId) const asset = fileNameToAsset.get(fileName) if (asset) { manifest[originalName] = asset