Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: move CSS emitFile logic closer to rollup #10909

Merged
merged 1 commit into from Nov 14, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
149 changes: 0 additions & 149 deletions packages/vite/src/node/__tests__/asset.spec.ts

This file was deleted.

125 changes: 7 additions & 118 deletions packages/vite/src/node/plugins/asset.ts
Expand Up @@ -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'
Expand All @@ -29,10 +27,15 @@ const urlRE = /(\?|&)url(?:&|$)/
const assetCache = new WeakMap<ResolvedConfig, Map<string, string>>()

// 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<string, { originalName: string }>
Map<string, GeneratedAssetMeta>
>()

// add own dictionary entry by directly assigning mrmime
Expand Down Expand Up @@ -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<OutputOptions['assetFileNames'], undefined> =
(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<OutputOptions['assetFileNames'], undefined>,
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
Expand Down
28 changes: 10 additions & 18 deletions packages/vite/src/node/plugins/css.ts
Expand Up @@ -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'

Expand Down Expand Up @@ -153,8 +152,6 @@ export const removedPureCssFilesCache = new WeakMap<
Map<string, RenderedChunk>
>()

export const cssEntryFilesCache = new WeakMap<ResolvedConfig, Set<string>>()

const postcssConfigCache: Record<
string,
WeakMap<ResolvedConfig, PostCSSConfigResult | null>
Expand Down Expand Up @@ -190,7 +187,6 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
cssModulesCache.set(config, moduleCache)

removedPureCssFilesCache.set(config, new Map<string, RenderedChunk>())
cssEntryFilesCache.set(config, new Set())
},

async transform(raw, id, options) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down