Skip to content

Commit

Permalink
feat(asset): add ?inline and ?no-inline queries to control inlini…
Browse files Browse the repository at this point in the history
…ng (#15454)

Co-authored-by: bluwy <bjornlu.dev@gmail.com>
Co-authored-by: 翠 / green <green@sapphi.red>
  • Loading branch information
3 people authored Nov 4, 2024
1 parent fb227ec commit 9162172
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 31 deletions.
11 changes: 11 additions & 0 deletions docs/guide/assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions packages/vite/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
68 changes: 49 additions & 19 deletions packages/vite/src/node/plugins/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)!
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
),
)
}
Expand Down
42 changes: 31 additions & 11 deletions playground/assets/__tests__/assets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,/)
})
})

Expand All @@ -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 () => {
Expand Down
24 changes: 24 additions & 0 deletions playground/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,18 @@ <h2>Unknown extension assets import</h2>
<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>

Expand Down Expand Up @@ -476,6 +488,18 @@ <h3>assets in template</h3>
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)

Expand Down

0 comments on commit 9162172

Please sign in to comment.