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
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
5 changes: 5 additions & 0 deletions packages/vite/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ declare module '*?inline' {
export default src
}

declare module '*?no-inline' {
const src: string
export default src
}

declare interface VitePreloadErrorEvent extends Event {
payload: Error
}
Expand Down
56 changes: 39 additions & 17 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,19 @@ 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()

let rtn: string
if (checkPublicFile(id, config)) {
// in public dir during dev, keep the url as-is
Expand All @@ -274,6 +279,13 @@ export function fileToDevUrl(
// (this is special handled by the serve static middleware
rtn = path.posix.join(FS_PREFIX, id)
}

if (inlineRE.test(id)) {
const file = cleanUrl(id)
const content = await fsp.readFile(file)
return assetToDataURL(environment, file, content)
}

if (skipBase) {
return rtn
}
Expand Down Expand Up @@ -350,19 +362,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 +414,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 +433,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 @@ -1041,7 +1041,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
16 changes: 16 additions & 0 deletions playground/assets/__tests__/assets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,22 @@ 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')).startsWith(
'data:image/png;base64,',
),
).toBe(true)
})

test('?url import', async () => {
const src = readFile('foo.js')
expect(await page.textContent('.url')).toMatch(
Expand Down
12 changes: 12 additions & 0 deletions playground/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ <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>?url import</h2>
<code class="url"></code>

Expand Down Expand Up @@ -476,6 +482,12 @@ <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 fooUrl from './foo.js?url'
text('.url', fooUrl)

Expand Down