mirror of
https://github.com/vitejs/vite.git
synced 2024-11-21 22:59:10 +00:00
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:
parent
fb227ec440
commit
9162172e03
@ -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.
|
||||
|
10
packages/vite/client.d.ts
vendored
10
packages/vite/client.d.ts
vendored
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -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 () => {
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user