Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 15 commits into from
Nov 4, 2024
Merged
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