mirror of
https://github.com/vitejs/vite.git
synced 2024-11-21 14:48:41 +00:00
feat(asset): inline svg in dev if within limit (#18581)
This commit is contained in:
parent
3ef0bf19a3
commit
f08b1463db
@ -120,7 +120,10 @@ export interface BuildEnvironmentOptions {
|
||||
assetsDir?: string
|
||||
/**
|
||||
* Static asset files smaller than this number (in bytes) will be inlined as
|
||||
* base64 strings. Default limit is `4096` (4 KiB). Set to `0` to disable.
|
||||
* base64 strings. If a callback is passed, a boolean can be returned to opt-in
|
||||
* or opt-out of inlining. If nothing is returned the default logic applies.
|
||||
*
|
||||
* Default limit is `4096` (4 KiB). Set to `0` to disable.
|
||||
* @default 4096
|
||||
*/
|
||||
assetsInlineLimit?:
|
||||
|
@ -38,6 +38,7 @@ const jsSourceMapRE = /\.[cm]?js\.map$/
|
||||
|
||||
const noInlineRE = /[?&]no-inline\b/
|
||||
const inlineRE = /[?&]inline\b/
|
||||
const svgExtRE = /\.svg(?:$|\?)/
|
||||
|
||||
const assetCache = new WeakMap<Environment, Map<string, string>>()
|
||||
|
||||
@ -180,11 +181,11 @@ export function assetPlugin(config: ResolvedConfig): Plugin {
|
||||
let url = await fileToUrl(this, id)
|
||||
|
||||
// Inherit HMR timestamp if this asset was invalidated
|
||||
const environment = this.environment
|
||||
const mod =
|
||||
environment.mode === 'dev' && environment.moduleGraph.getModuleById(id)
|
||||
if (mod && mod.lastHMRTimestamp > 0) {
|
||||
url = injectQuery(url, `t=${mod.lastHMRTimestamp}`)
|
||||
if (!url.startsWith('data:') && this.environment.mode === 'dev') {
|
||||
const mod = this.environment.moduleGraph.getModuleById(id)
|
||||
if (mod && mod.lastHMRTimestamp > 0) {
|
||||
url = injectQuery(url, `t=${mod.lastHMRTimestamp}`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@ -266,16 +267,27 @@ export async function fileToDevUrl(
|
||||
skipBase = false,
|
||||
): Promise<string> {
|
||||
const config = environment.getTopLevelConfig()
|
||||
const publicFile = checkPublicFile(id, config)
|
||||
|
||||
// If has inline query, unconditionally inline the asset
|
||||
if (inlineRE.test(id)) {
|
||||
const file = checkPublicFile(id, config) || cleanUrl(id)
|
||||
const file = publicFile || cleanUrl(id)
|
||||
const content = await fsp.readFile(file)
|
||||
return assetToDataURL(environment, file, content)
|
||||
}
|
||||
|
||||
// If is svg and it's inlined in build, also inline it in dev to match
|
||||
// the behaviour in build due to quote handling differences.
|
||||
if (svgExtRE.test(id)) {
|
||||
const file = publicFile || cleanUrl(id)
|
||||
const content = await fsp.readFile(file)
|
||||
if (shouldInline(environment, file, id, content, undefined, undefined)) {
|
||||
return assetToDataURL(environment, file, content)
|
||||
}
|
||||
}
|
||||
|
||||
let rtn: string
|
||||
if (checkPublicFile(id, config)) {
|
||||
if (publicFile) {
|
||||
// in public dir during dev, keep the url as-is
|
||||
rtn = id
|
||||
} else if (id.startsWith(withTrailingSlash(config.root))) {
|
||||
@ -369,7 +381,9 @@ async function fileToBuiltUrl(
|
||||
const content = await fsp.readFile(file)
|
||||
|
||||
let url: string
|
||||
if (shouldInline(pluginContext, file, id, content, forceInline)) {
|
||||
if (
|
||||
shouldInline(environment, file, id, content, pluginContext, forceInline)
|
||||
) {
|
||||
url = assetToDataURL(environment, file, content)
|
||||
} else {
|
||||
// emit as asset
|
||||
@ -413,21 +427,28 @@ export async function urlToBuiltUrl(
|
||||
)
|
||||
}
|
||||
|
||||
const shouldInline = (
|
||||
pluginContext: PluginContext,
|
||||
function shouldInline(
|
||||
environment: Environment,
|
||||
file: string,
|
||||
id: string,
|
||||
content: Buffer,
|
||||
/** Should be passed only in build */
|
||||
buildPluginContext: PluginContext | undefined,
|
||||
forceInline: boolean | undefined,
|
||||
): boolean => {
|
||||
const environment = pluginContext.environment
|
||||
const { assetsInlineLimit } = environment.config.build
|
||||
): boolean {
|
||||
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
|
||||
// Do build only checks if passed the plugin context during build
|
||||
if (buildPluginContext) {
|
||||
if (environment.config.build.lib) return true
|
||||
if (buildPluginContext.getModuleInfo(id)?.isEntry) return false
|
||||
}
|
||||
if (forceInline !== undefined) return forceInline
|
||||
if (file.endsWith('.html')) return false
|
||||
// Don't inline SVG with fragments, as they are meant to be reused
|
||||
if (file.endsWith('.svg') && id.includes('#')) return false
|
||||
let limit: number
|
||||
const { assetsInlineLimit } = environment.config.build
|
||||
if (typeof assetsInlineLimit === 'function') {
|
||||
const userShouldInline = assetsInlineLimit(file, content)
|
||||
if (userShouldInline != null) return userShouldInline
|
||||
@ -435,9 +456,6 @@ const shouldInline = (
|
||||
} else {
|
||||
limit = Number(assetsInlineLimit)
|
||||
}
|
||||
if (file.endsWith('.html')) return false
|
||||
// Don't inline SVG with fragments, as they are meant to be reused
|
||||
if (file.endsWith('.svg') && id.includes('#')) return false
|
||||
return content.length < limit && !isGitLfsPlaceholder(content)
|
||||
}
|
||||
|
||||
|
@ -282,15 +282,11 @@ describe('css url() references', () => {
|
||||
})
|
||||
|
||||
test('url() with svg', async () => {
|
||||
expect(await getBg('.css-url-svg')).toMatch(
|
||||
isBuild ? /data:image\/svg\+xml,.+/ : '/foo/bar/nested/fragment-bg.svg',
|
||||
)
|
||||
expect(await getBg('.css-url-svg')).toMatch(/data:image\/svg\+xml,.+/)
|
||||
})
|
||||
|
||||
test('image-set() with svg', async () => {
|
||||
expect(await getBg('.css-image-set-svg')).toMatch(
|
||||
isBuild ? /data:image\/svg\+xml,.+/ : '/foo/bar/nested/fragment-bg.svg',
|
||||
)
|
||||
expect(await getBg('.css-image-set-svg')).toMatch(/data:image\/svg\+xml,.+/)
|
||||
})
|
||||
})
|
||||
|
||||
@ -376,10 +372,8 @@ describe('svg fragments', () => {
|
||||
test('from js import', async () => {
|
||||
const img = await page.$('.svg-frag-import')
|
||||
expect(await img.getAttribute('src')).toMatch(
|
||||
isBuild
|
||||
? // Assert trimmed (data URI starts with < and ends with >)
|
||||
/^data:image\/svg\+xml,%3c.*%3e#icon-heart-view$/
|
||||
: /svg#icon-heart-view$/,
|
||||
// Assert trimmed (data URI starts with < and ends with >)
|
||||
/^data:image\/svg\+xml,%3c.*%3e#icon-heart-view$/,
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -884,9 +884,24 @@ if (!isBuild) {
|
||||
)
|
||||
})
|
||||
|
||||
test('assets HMR', async () => {
|
||||
test('not inlined assets HMR', async () => {
|
||||
await setupModuleRunner('/hmr.ts')
|
||||
const el = () => hmr('#logo-no-inline')
|
||||
await untilConsoleLogAfter(
|
||||
() =>
|
||||
editFile('logo-no-inline.svg', (code) =>
|
||||
code.replace('height="30px"', 'height="40px"'),
|
||||
),
|
||||
/Logo-no-inline updated/,
|
||||
)
|
||||
await vi.waitUntil(() => el().includes('logo-no-inline.svg?t='))
|
||||
})
|
||||
|
||||
test('inlined assets HMR', async () => {
|
||||
await setupModuleRunner('/hmr.ts')
|
||||
const el = () => hmr('#logo')
|
||||
const initialLogoUrl = el()
|
||||
expect(initialLogoUrl).toMatch(/^data:image\/svg\+xml/)
|
||||
await untilConsoleLogAfter(
|
||||
() =>
|
||||
editFile('logo.svg', (code) =>
|
||||
@ -894,7 +909,10 @@ if (!isBuild) {
|
||||
),
|
||||
/Logo updated/,
|
||||
)
|
||||
await vi.waitUntil(() => el().includes('logo.svg?t='))
|
||||
// Should be updated with new data url
|
||||
const updatedLogoUrl = el()
|
||||
expect(updatedLogoUrl).toMatch(/^data:image\/svg\+xml/)
|
||||
expect(updatedLogoUrl).not.toEqual(initialLogoUrl)
|
||||
})
|
||||
} else {
|
||||
test('this file only includes test for serve', () => {
|
||||
|
@ -8,6 +8,7 @@ import './intermediate-file-delete'
|
||||
import './circular'
|
||||
import './queries'
|
||||
import logo from './logo.svg'
|
||||
import logoNoInline from './logo-no-inline.svg'
|
||||
import { msg as softInvalidationMsg } from './soft-invalidation'
|
||||
|
||||
export const foo = 1
|
||||
@ -16,7 +17,8 @@ text('.dep', depFoo)
|
||||
text('.nested', nestedFoo)
|
||||
text('.virtual', virtual)
|
||||
text('.soft-invalidation', softInvalidationMsg)
|
||||
setLogo(logo)
|
||||
setImgSrc('#logo', logo)
|
||||
setImgSrc('#logo-no-inline', logoNoInline)
|
||||
|
||||
globalThis.__HMR__['virtual:increment'] = () => {
|
||||
if (import.meta.hot) {
|
||||
@ -41,10 +43,15 @@ if (import.meta.hot) {
|
||||
}
|
||||
|
||||
import.meta.hot.accept('./logo.svg', (newUrl) => {
|
||||
setLogo(newUrl.default)
|
||||
setImgSrc('#logo', newUrl.default)
|
||||
log('Logo updated', newUrl.default)
|
||||
})
|
||||
|
||||
import.meta.hot.accept('./logo-no-inline.svg', (newUrl) => {
|
||||
setImgSrc('#logo-no-inline', newUrl.default)
|
||||
log('Logo-no-inline updated', newUrl.default)
|
||||
})
|
||||
|
||||
import.meta.hot.accept('./hmrDep', ({ foo, nestedFoo }) => {
|
||||
handleDep('single dep', foo, nestedFoo)
|
||||
})
|
||||
@ -98,8 +105,8 @@ function text(el, text) {
|
||||
hmr(el, text)
|
||||
}
|
||||
|
||||
function setLogo(src) {
|
||||
hmr('#logo', src)
|
||||
function setImgSrc(el, src) {
|
||||
hmr(el, src)
|
||||
}
|
||||
|
||||
function removeCb({ msg }) {
|
||||
|
3
playground/hmr-ssr/logo-no-inline.svg
Normal file
3
playground/hmr-ssr/logo-no-inline.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 30 30" height="30px" width="30px" xmlns="http://www.w3.org/2000/svg">
|
||||
<text x="1" y="20" fill="#646cff" font-size="16px">Vite</text>
|
||||
</svg>
|
After Width: | Height: | Size: 162 B |
@ -5,6 +5,13 @@ export default defineConfig({
|
||||
experimental: {
|
||||
hmrPartialAccept: true,
|
||||
},
|
||||
build: {
|
||||
assetsInlineLimit(filePath) {
|
||||
if (filePath.endsWith('logo-no-inline.svg')) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'mock-custom',
|
||||
|
@ -998,7 +998,20 @@ if (!isBuild) {
|
||||
)
|
||||
})
|
||||
|
||||
test('assets HMR', async () => {
|
||||
test('not inlined assets HMR', async () => {
|
||||
await page.goto(viteTestUrl)
|
||||
const el = await page.$('#logo-no-inline')
|
||||
await untilBrowserLogAfter(
|
||||
() =>
|
||||
editFile('logo-no-inline.svg', (code) =>
|
||||
code.replace('height="30px"', 'height="40px"'),
|
||||
),
|
||||
/Logo-no-inline updated/,
|
||||
)
|
||||
await untilUpdated(() => el.evaluate((it) => `${it.clientHeight}`), '40')
|
||||
})
|
||||
|
||||
test('inlined assets HMR', async () => {
|
||||
await page.goto(viteTestUrl)
|
||||
const el = await page.$('#logo')
|
||||
await untilBrowserLogAfter(
|
||||
|
@ -6,6 +6,7 @@ import './optional-chaining/parent'
|
||||
import './intermediate-file-delete'
|
||||
import './circular'
|
||||
import logo from './logo.svg'
|
||||
import logoNoInline from './logo-no-inline.svg'
|
||||
import { msg as softInvalidationMsg } from './soft-invalidation'
|
||||
|
||||
export const foo = 1
|
||||
@ -14,7 +15,8 @@ text('.dep', depFoo)
|
||||
text('.nested', nestedFoo)
|
||||
text('.virtual', virtual)
|
||||
text('.soft-invalidation', softInvalidationMsg)
|
||||
setLogo(logo)
|
||||
setImgSrc('#logo', logo)
|
||||
setImgSrc('#logo-no-inline', logoNoInline)
|
||||
|
||||
const btn = document.querySelector('.virtual-update') as HTMLButtonElement
|
||||
btn.onclick = () => {
|
||||
@ -40,10 +42,15 @@ if (import.meta.hot) {
|
||||
}
|
||||
|
||||
import.meta.hot.accept('./logo.svg', (newUrl) => {
|
||||
setLogo(newUrl.default)
|
||||
setImgSrc('#logo', newUrl.default)
|
||||
console.log('Logo updated', newUrl.default)
|
||||
})
|
||||
|
||||
import.meta.hot.accept('./logo-no-inline.svg', (newUrl) => {
|
||||
setImgSrc('#logo-no-inline', newUrl.default)
|
||||
console.log('Logo-no-inline updated', newUrl.default)
|
||||
})
|
||||
|
||||
import.meta.hot.accept('./hmrDep', ({ foo, nestedFoo }) => {
|
||||
handleDep('single dep', foo, nestedFoo)
|
||||
})
|
||||
@ -131,8 +138,8 @@ function text(el, text) {
|
||||
document.querySelector(el).textContent = text
|
||||
}
|
||||
|
||||
function setLogo(src) {
|
||||
;(document.querySelector('#logo') as HTMLImageElement).src = src
|
||||
function setImgSrc(el, src) {
|
||||
;(document.querySelector(el) as HTMLImageElement).src = src
|
||||
}
|
||||
|
||||
function removeCb({ msg }) {
|
||||
|
@ -38,5 +38,6 @@
|
||||
<div class="optional-chaining"></div>
|
||||
<button class="intermediate-file-delete-increment">1</button>
|
||||
<div class="intermediate-file-delete-display"></div>
|
||||
<image id="logo"></image>
|
||||
<img id="logo" />
|
||||
<img id="logo-no-inline" />
|
||||
<div class="circular"></div>
|
||||
|
3
playground/hmr/logo-no-inline.svg
Normal file
3
playground/hmr/logo-no-inline.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 30 30" height="30px" width="30px" xmlns="http://www.w3.org/2000/svg">
|
||||
<text x="1" y="20" fill="#646cff" font-size="16px">Vite</text>
|
||||
</svg>
|
After Width: | Height: | Size: 162 B |
@ -7,6 +7,13 @@ export default defineConfig({
|
||||
experimental: {
|
||||
hmrPartialAccept: true,
|
||||
},
|
||||
build: {
|
||||
assetsInlineLimit(filePath) {
|
||||
if (filePath.endsWith('logo-no-inline.svg')) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'mock-custom',
|
||||
|
Loading…
Reference in New Issue
Block a user