Skip to content

Commit 9162172

Browse files
hugoattalbluwysapphi-red
authoredNov 4, 2024··
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>
1 parent fb227ec commit 9162172

File tree

6 files changed

+130
-31
lines changed

6 files changed

+130
-31
lines changed
 

‎docs/guide/assets.md

+11
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ import workletURL from 'extra-scalloped-border/worklet.js?url'
5555
CSS.paintWorklet.addModule(workletURL)
5656
```
5757

58+
### Explicit Inline Handling
59+
60+
Assets can be explicitly imported with inlining or no inlining using the `?inline` or `?no-inline` suffix respectively.
61+
62+
```js twoslash
63+
import 'vite/client'
64+
// ---cut---
65+
import imgUrl1 from './img.svg?no-inline'
66+
import imgUrl2 from './img.png?inline'
67+
```
68+
5869
### Importing Asset as String
5970

6071
Assets can be imported as strings using the `?raw` suffix.

‎packages/vite/client.d.ts

+10
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,16 @@ declare module '*?inline' {
247247
export default src
248248
}
249249

250+
declare module '*?no-inline' {
251+
const src: string
252+
export default src
253+
}
254+
255+
declare module '*?url&inline' {
256+
const src: string
257+
export default src
258+
}
259+
250260
declare interface VitePreloadErrorEvent extends Event {
251261
payload: Error
252262
}

‎packages/vite/src/node/plugins/asset.ts

+49-19
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export const assetUrlRE = /__VITE_ASSET__([\w$]+)__(?:\$_(.*?)__)?/g
3636

3737
const jsSourceMapRE = /\.[cm]?js\.map$/
3838

39+
const noInlineRE = /[?&]no-inline\b/
40+
const inlineRE = /[?&]inline\b/
41+
3942
const assetCache = new WeakMap<Environment, Map<string, string>>()
4043

4144
/** a set of referenceId for entry CSS assets for each environment */
@@ -251,17 +254,26 @@ export async function fileToUrl(
251254
): Promise<string> {
252255
const { environment } = pluginContext
253256
if (environment.config.command === 'serve') {
254-
return fileToDevUrl(id, environment.getTopLevelConfig())
257+
return fileToDevUrl(environment, id)
255258
} else {
256259
return fileToBuiltUrl(pluginContext, id)
257260
}
258261
}
259262

260-
export function fileToDevUrl(
263+
export async function fileToDevUrl(
264+
environment: Environment,
261265
id: string,
262-
config: ResolvedConfig,
263266
skipBase = false,
264-
): string {
267+
): Promise<string> {
268+
const config = environment.getTopLevelConfig()
269+
270+
// If has inline query, unconditionally inline the asset
271+
if (inlineRE.test(id)) {
272+
const file = checkPublicFile(id, config) || cleanUrl(id)
273+
const content = await fsp.readFile(file)
274+
return assetToDataURL(environment, file, content)
275+
}
276+
265277
let rtn: string
266278
if (checkPublicFile(id, config)) {
267279
// in public dir during dev, keep the url as-is
@@ -335,8 +347,16 @@ async function fileToBuiltUrl(
335347
): Promise<string> {
336348
const environment = pluginContext.environment
337349
const topLevelConfig = environment.getTopLevelConfig()
338-
if (!skipPublicCheck && checkPublicFile(id, topLevelConfig)) {
339-
return publicFileToBuiltUrl(id, topLevelConfig)
350+
if (!skipPublicCheck) {
351+
const publicFile = checkPublicFile(id, topLevelConfig)
352+
if (publicFile) {
353+
if (inlineRE.test(id)) {
354+
// If inline via query, re-assign the id so it can be read by the fs and inlined
355+
id = publicFile
356+
} else {
357+
return publicFileToBuiltUrl(id, topLevelConfig)
358+
}
359+
}
340360
}
341361

342362
const cache = assetCache.get(environment)!
@@ -350,19 +370,7 @@ async function fileToBuiltUrl(
350370

351371
let url: string
352372
if (shouldInline(pluginContext, file, id, content, forceInline)) {
353-
if (environment.config.build.lib && isGitLfsPlaceholder(content)) {
354-
environment.logger.warn(
355-
colors.yellow(`Inlined file ${id} was not downloaded via Git LFS`),
356-
)
357-
}
358-
359-
if (file.endsWith('.svg')) {
360-
url = svgToDataURL(content)
361-
} else {
362-
const mimeType = mrmime.lookup(file) ?? 'application/octet-stream'
363-
// base64 inlined as a string
364-
url = `data:${mimeType};base64,${content.toString('base64')}`
365-
}
373+
url = assetToDataURL(environment, file, content)
366374
} else {
367375
// emit as asset
368376
const originalFileName = normalizePath(
@@ -414,6 +422,8 @@ const shouldInline = (
414422
): boolean => {
415423
const environment = pluginContext.environment
416424
const { assetsInlineLimit } = environment.config.build
425+
if (noInlineRE.test(id)) return false
426+
if (inlineRE.test(id)) return true
417427
if (environment.config.build.lib) return true
418428
if (pluginContext.getModuleInfo(id)?.isEntry) return false
419429
if (forceInline !== undefined) return forceInline
@@ -431,6 +441,26 @@ const shouldInline = (
431441
return content.length < limit && !isGitLfsPlaceholder(content)
432442
}
433443

444+
function assetToDataURL(
445+
environment: Environment,
446+
file: string,
447+
content: Buffer,
448+
) {
449+
if (environment.config.build.lib && isGitLfsPlaceholder(content)) {
450+
environment.logger.warn(
451+
colors.yellow(`Inlined file ${file} was not downloaded via Git LFS`),
452+
)
453+
}
454+
455+
if (file.endsWith('.svg')) {
456+
return svgToDataURL(content)
457+
} else {
458+
const mimeType = mrmime.lookup(file) ?? 'application/octet-stream'
459+
// base64 inlined as a string
460+
return `data:${mimeType};base64,${content.toString('base64')}`
461+
}
462+
}
463+
434464
const nestedQuotesRE = /"[^"']*'[^"]*"|'[^'"]*"[^']*'/
435465

436466
// Inspired by https://github.com/iconify/iconify/blob/main/packages/utils/src/svg/url.ts

‎packages/vite/src/node/plugins/css.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1042,7 +1042,11 @@ export function cssAnalysisPlugin(config: ResolvedConfig): Plugin {
10421042
isCSSRequest(file)
10431043
? moduleGraph.createFileOnlyEntry(file)
10441044
: await moduleGraph.ensureEntryFromUrl(
1045-
fileToDevUrl(file, config, /* skipBase */ true),
1045+
await fileToDevUrl(
1046+
this.environment,
1047+
file,
1048+
/* skipBase */ true,
1049+
),
10461050
),
10471051
)
10481052
}

‎playground/assets/__tests__/assets.spec.ts

+31-11
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,32 @@ test('?raw import', async () => {
400400
expect(await page.textContent('.raw')).toMatch('SVG')
401401
})
402402

403+
test('?no-inline svg import', async () => {
404+
expect(await page.textContent('.no-inline-svg')).toMatch(
405+
isBuild
406+
? /\/foo\/bar\/assets\/fragment-[-\w]{8}\.svg\?no-inline/
407+
: '/foo/bar/nested/fragment.svg?no-inline',
408+
)
409+
})
410+
411+
test('?inline png import', async () => {
412+
expect(await page.textContent('.inline-png')).toMatch(
413+
/^data:image\/png;base64,/,
414+
)
415+
})
416+
417+
test('?inline public png import', async () => {
418+
expect(await page.textContent('.inline-public-png')).toMatch(
419+
/^data:image\/png;base64,/,
420+
)
421+
})
422+
423+
test('?inline public json import', async () => {
424+
expect(await page.textContent('.inline-public-json')).toMatch(
425+
/^data:application\/json;base64,/,
426+
)
427+
})
428+
403429
test('?url import', async () => {
404430
const src = readFile('foo.js')
405431
expect(await page.textContent('.url')).toMatch(
@@ -432,9 +458,7 @@ describe('unicode url', () => {
432458
describe.runIf(isBuild)('encodeURI', () => {
433459
test('img src with encodeURI', async () => {
434460
const img = await page.$('.encodeURI')
435-
expect(
436-
(await img.getAttribute('src')).startsWith('data:image/png;base64'),
437-
).toBe(true)
461+
expect(await img.getAttribute('src')).toMatch(/^data:image\/png;base64,/)
438462
})
439463
})
440464

@@ -454,14 +478,10 @@ test('new URL("/...", import.meta.url)', async () => {
454478

455479
test('new URL("data:...", import.meta.url)', async () => {
456480
const img = await page.$('.import-meta-url-data-uri-img')
457-
expect(
458-
(await img.getAttribute('src')).startsWith('data:image/png;base64'),
459-
).toBe(true)
460-
expect(
461-
(await page.textContent('.import-meta-url-data-uri')).startsWith(
462-
'data:image/png;base64',
463-
),
464-
).toBe(true)
481+
expect(await img.getAttribute('src')).toMatch(/^data:image\/png;base64,/)
482+
expect(await page.textContent('.import-meta-url-data-uri')).toMatch(
483+
/^data:image\/png;base64,/,
484+
)
465485
})
466486

467487
test('new URL(..., import.meta.url) without extension', async () => {

‎playground/assets/index.html

+24
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,18 @@ <h2>Unknown extension assets import</h2>
237237
<h2>?raw import</h2>
238238
<code class="raw"></code>
239239

240+
<h2>?no-inline svg import</h2>
241+
<code class="no-inline-svg"></code>
242+
243+
<h2>?inline png import</h2>
244+
<code class="inline-png"></code>
245+
246+
<h2>?inline public png import</h2>
247+
<code class="inline-public-png"></code>
248+
249+
<h2>?url&inline public json import</h2>
250+
<code class="inline-public-json"></code>
251+
240252
<h2>?url import</h2>
241253
<code class="url"></code>
242254

@@ -476,6 +488,18 @@ <h3>assets in template</h3>
476488
import rawSvg from './nested/fragment.svg?raw'
477489
text('.raw', rawSvg)
478490

491+
import noInlineSvg from './nested/fragment.svg?no-inline'
492+
text('.no-inline-svg', noInlineSvg)
493+
494+
import inlinePng from './nested/asset.png?inline'
495+
text('.inline-png', inlinePng)
496+
497+
import inlinePublicPng from '/icon.png?inline'
498+
text('.inline-public-png', inlinePublicPng)
499+
500+
import inlinePublicJson from '/foo.json?url&inline'
501+
text('.inline-public-json', inlinePublicJson)
502+
479503
import fooUrl from './foo.js?url'
480504
text('.url', fooUrl)
481505

0 commit comments

Comments
 (0)
Please sign in to comment.