Skip to content

Commit

Permalink
fix(build): respect rollup output.assetFileNames, fix #2944 (#4352)
Browse files Browse the repository at this point in the history
  • Loading branch information
SegaraRai committed Aug 2, 2021
1 parent f9e5d63 commit cbd0458
Show file tree
Hide file tree
Showing 2 changed files with 244 additions and 6 deletions.
156 changes: 156 additions & 0 deletions packages/vite/src/node/__tests__/asset.spec.ts
@@ -0,0 +1,156 @@
import { assetFileNamesToFileName, getAssetHash } from '../plugins/asset'

describe('getAssetHash', () => {
test('8-digit hex', () => {
const hash = getAssetHash(Buffer.alloc(0))

expect(hash).toMatch(/^[\da-f]{8}$/)
})
})

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')
})
}
})
94 changes: 88 additions & 6 deletions packages/vite/src/node/plugins/asset.ts
Expand Up @@ -6,7 +6,7 @@ import { Plugin } from '../plugin'
import { ResolvedConfig } from '../config'
import { cleanUrl } from '../utils'
import { FS_PREFIX } from '../constants'
import { PluginContext, RenderedChunk } from 'rollup'
import { OutputOptions, PluginContext, RenderedChunk } from 'rollup'
import MagicString from 'magic-string'
import { createHash } from 'crypto'

Expand Down Expand Up @@ -185,6 +185,82 @@ export function getAssetFilename(
return assetHashToFilenameMap.get(config)?.get(hash)
}

/**
* 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',
* getAssetHash(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.substr(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 name
}
throw new Error(
`invalid placeholder ${placeholder} in assetFileNames "${assetFileNames}"`
)
}
)

return fileName
}

/**
* Register an asset to be emitted as part of the bundle (if necessary)
* and returns the resolved public URL
Expand Down Expand Up @@ -228,11 +304,17 @@ async function fileToBuiltUrl(
const contentHash = getAssetHash(content)
const { search, hash } = parseUrl(id)
const postfix = (search || '') + (hash || '')
const basename = path.basename(file)
const ext = path.extname(basename)
const fileName = path.posix.join(
config.build.assetsDir,
`${basename.slice(0, -ext.length)}.${contentHash}${ext}`
const output = config.build?.rollupOptions?.output
const assetFileNames =
(output && !Array.isArray(output) ? output.assetFileNames : undefined) ??
// defaults to '<assetsDir>/[name].[hash][extname]'
// slightly different from rollup's one ('assets/[name]-[hash][extname]')
path.posix.join(config.build.assetsDir, '[name].[hash][extname]')
const fileName = assetFileNamesToFileName(
assetFileNames,
file,
contentHash,
content
)
if (!map.has(contentHash)) {
map.set(contentHash, fileName)
Expand Down

0 comments on commit cbd0458

Please sign in to comment.