feat(asset): add ?inline and ?no-inline queries to control inlining (#15454)

Co-authored-by: bluwy <bjornlu.dev@gmail.com>
Co-authored-by: 翠 / green <green@sapphi.red>
This commit is contained in:
Hugo ATTAL 2024-11-04 15:50:15 +01:00 committed by GitHub
parent fb227ec440
commit 9162172e03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 130 additions and 31 deletions

View File

@ -55,6 +55,17 @@ import workletURL from 'extra-scalloped-border/worklet.js?url'
CSS.paintWorklet.addModule(workletURL)
```
### Explicit Inline Handling
Assets can be explicitly imported with inlining or no inlining using the `?inline` or `?no-inline` suffix respectively.
```js twoslash
import 'vite/client'
// ---cut---
import imgUrl1 from './img.svg?no-inline'
import imgUrl2 from './img.png?inline'
```
### Importing Asset as String
Assets can be imported as strings using the `?raw` suffix.

View File

@ -247,6 +247,16 @@ declare module '*?inline' {
export default src
}
declare module '*?no-inline' {
const src: string
export default src
}
declare module '*?url&inline' {
const src: string
export default src
}
declare interface VitePreloadErrorEvent extends Event {
payload: Error
}

View File

@ -36,6 +36,9 @@ export const assetUrlRE = /__VITE_ASSET__([\w$]+)__(?:\$_(.*?)__)?/g
const jsSourceMapRE = /\.[cm]?js\.map$/
const noInlineRE = /[?&]no-inline\b/
const inlineRE = /[?&]inline\b/
const assetCache = new WeakMap<Environment, Map<string, string>>()
/** a set of referenceId for entry CSS assets for each environment */
@ -251,17 +254,26 @@ export async function fileToUrl(
): Promise<string> {
const { environment } = pluginContext
if (environment.config.command === 'serve') {
return fileToDevUrl(id, environment.getTopLevelConfig())
return fileToDevUrl(environment, id)
} else {
return fileToBuiltUrl(pluginContext, id)
}
}
export function fileToDevUrl(
export async function fileToDevUrl(
environment: Environment,
id: string,
config: ResolvedConfig,
skipBase = false,
): string {
): Promise<string> {
const config = environment.getTopLevelConfig()
// If has inline query, unconditionally inline the asset
if (inlineRE.test(id)) {
const file = checkPublicFile(id, config) || cleanUrl(id)
const content = await fsp.readFile(file)
return assetToDataURL(environment, file, content)
}
let rtn: string
if (checkPublicFile(id, config)) {
// in public dir during dev, keep the url as-is
@ -335,8 +347,16 @@ async function fileToBuiltUrl(
): Promise<string> {
const environment = pluginContext.environment
const topLevelConfig = environment.getTopLevelConfig()
if (!skipPublicCheck && checkPublicFile(id, topLevelConfig)) {
return publicFileToBuiltUrl(id, topLevelConfig)
if (!skipPublicCheck) {
const publicFile = checkPublicFile(id, topLevelConfig)
if (publicFile) {
if (inlineRE.test(id)) {
// If inline via query, re-assign the id so it can be read by the fs and inlined
id = publicFile
} else {
return publicFileToBuiltUrl(id, topLevelConfig)
}
}
}
const cache = assetCache.get(environment)!
@ -350,19 +370,7 @@ async function fileToBuiltUrl(
let url: string
if (shouldInline(pluginContext, file, id, content, forceInline)) {
if (environment.config.build.lib && isGitLfsPlaceholder(content)) {
environment.logger.warn(
colors.yellow(`Inlined file ${id} was not downloaded via Git LFS`),
)
}
if (file.endsWith('.svg')) {
url = svgToDataURL(content)
} else {
const mimeType = mrmime.lookup(file) ?? 'application/octet-stream'
// base64 inlined as a string
url = `data:${mimeType};base64,${content.toString('base64')}`
}
url = assetToDataURL(environment, file, content)
} else {
// emit as asset
const originalFileName = normalizePath(
@ -414,6 +422,8 @@ const shouldInline = (
): boolean => {
const environment = pluginContext.environment
const { assetsInlineLimit } = environment.config.build
if (noInlineRE.test(id)) return false
if (inlineRE.test(id)) return true
if (environment.config.build.lib) return true
if (pluginContext.getModuleInfo(id)?.isEntry) return false
if (forceInline !== undefined) return forceInline
@ -431,6 +441,26 @@ const shouldInline = (
return content.length < limit && !isGitLfsPlaceholder(content)
}
function assetToDataURL(
environment: Environment,
file: string,
content: Buffer,
) {
if (environment.config.build.lib && isGitLfsPlaceholder(content)) {
environment.logger.warn(
colors.yellow(`Inlined file ${file} was not downloaded via Git LFS`),
)
}
if (file.endsWith('.svg')) {
return svgToDataURL(content)
} else {
const mimeType = mrmime.lookup(file) ?? 'application/octet-stream'
// base64 inlined as a string
return `data:${mimeType};base64,${content.toString('base64')}`
}
}
const nestedQuotesRE = /"[^"']*'[^"]*"|'[^'"]*"[^']*'/
// Inspired by https://github.com/iconify/iconify/blob/main/packages/utils/src/svg/url.ts

View File

@ -1042,7 +1042,11 @@ export function cssAnalysisPlugin(config: ResolvedConfig): Plugin {
isCSSRequest(file)
? moduleGraph.createFileOnlyEntry(file)
: await moduleGraph.ensureEntryFromUrl(
fileToDevUrl(file, config, /* skipBase */ true),
await fileToDevUrl(
this.environment,
file,
/* skipBase */ true,
),
),
)
}

View File

@ -400,6 +400,32 @@ test('?raw import', async () => {
expect(await page.textContent('.raw')).toMatch('SVG')
})
test('?no-inline svg import', async () => {
expect(await page.textContent('.no-inline-svg')).toMatch(
isBuild
? /\/foo\/bar\/assets\/fragment-[-\w]{8}\.svg\?no-inline/
: '/foo/bar/nested/fragment.svg?no-inline',
)
})
test('?inline png import', async () => {
expect(await page.textContent('.inline-png')).toMatch(
/^data:image\/png;base64,/,
)
})
test('?inline public png import', async () => {
expect(await page.textContent('.inline-public-png')).toMatch(
/^data:image\/png;base64,/,
)
})
test('?inline public json import', async () => {
expect(await page.textContent('.inline-public-json')).toMatch(
/^data:application\/json;base64,/,
)
})
test('?url import', async () => {
const src = readFile('foo.js')
expect(await page.textContent('.url')).toMatch(
@ -432,9 +458,7 @@ describe('unicode url', () => {
describe.runIf(isBuild)('encodeURI', () => {
test('img src with encodeURI', async () => {
const img = await page.$('.encodeURI')
expect(
(await img.getAttribute('src')).startsWith('data:image/png;base64'),
).toBe(true)
expect(await img.getAttribute('src')).toMatch(/^data:image\/png;base64,/)
})
})
@ -454,14 +478,10 @@ test('new URL("/...", import.meta.url)', async () => {
test('new URL("data:...", import.meta.url)', async () => {
const img = await page.$('.import-meta-url-data-uri-img')
expect(
(await img.getAttribute('src')).startsWith('data:image/png;base64'),
).toBe(true)
expect(
(await page.textContent('.import-meta-url-data-uri')).startsWith(
'data:image/png;base64',
),
).toBe(true)
expect(await img.getAttribute('src')).toMatch(/^data:image\/png;base64,/)
expect(await page.textContent('.import-meta-url-data-uri')).toMatch(
/^data:image\/png;base64,/,
)
})
test('new URL(..., import.meta.url) without extension', async () => {

View File

@ -237,6 +237,18 @@
<h2>?raw import</h2>
<code class="raw"></code>
<h2>?no-inline svg import</h2>
<code class="no-inline-svg"></code>
<h2>?inline png import</h2>
<code class="inline-png"></code>
<h2>?inline public png import</h2>
<code class="inline-public-png"></code>
<h2>?url&inline public json import</h2>
<code class="inline-public-json"></code>
<h2>?url import</h2>
<code class="url"></code>
@ -476,6 +488,18 @@
import rawSvg from './nested/fragment.svg?raw'
text('.raw', rawSvg)
import noInlineSvg from './nested/fragment.svg?no-inline'
text('.no-inline-svg', noInlineSvg)
import inlinePng from './nested/asset.png?inline'
text('.inline-png', inlinePng)
import inlinePublicPng from '/icon.png?inline'
text('.inline-public-png', inlinePublicPng)
import inlinePublicJson from '/foo.json?url&inline'
text('.inline-public-json', inlinePublicJson)
import fooUrl from './foo.js?url'
text('.url', fooUrl)