vite icon indicating copy to clipboard operation
vite copied to clipboard

Support inlining SVG assets

Open andylizi opened this issue 4 years ago • 8 comments

I was asked to open another issue for this.

Describe the bug

Vite doesn't inline svg files when the documentation says it would.

Reproduction

https://bitbucket.org/andylizi/test-vite-svg-inline/

Expected behavior

  • logo.svg should be inlined, as assetsInlineLimit claims to inline any static asset files under the default 4kb limit. https://github.com/vitejs/vite/blob/9904cb1a758578a508d1e45717b6293162397bb3/src/node/config.ts#L315-L320

Actual behavior

  • logo.svg is not inlined.

System Info

  • vite version: v1.0.0-rc.13
  • Operating System: Windows 10 (64-bit)
  • Node version: v14.8.0

Related code

https://github.com/vitejs/vite/blob/480367b83a5d418e76a4a6bccd004abb97413c22/src/node/build/buildPluginAsset.ts#L60-L63

Preferred solution

Adding support for svg inlining would be great. Unfortunately extra steps are required to do it properly, as #1197 mentioned: Probably Don’t Base64 SVG and Optimizing SVGs in data URIs.

Alternative solution

Document this behavior in config.ts so users wouldn't be surprised by this.

Workaround

Rename .svg to uppercase .SVG. This isn't ideal but it works for now.

andylizi avatar Dec 05 '20 08:12 andylizi

While this is in the process of getting fixed, here is my current patchy solution (leveraging webpack's svg-inline-loader 😅 )

Add svgLoader() to your plugins array and you're good to go!

import { getExtractedSVG } from "svg-inline-loader"
import type { Plugin } from "rollup"
import fs from "fs"

//TODO: remove this once https://github.com/vitejs/vite/pull/2909 gets merged
export const svgLoader: (options?: {
  classPrefix?: string
  idPrefix?: string
  removeSVGTagAttrs?: boolean
  warnTags?: boolean
  removeTags?: boolean
  warnTagAttrs?: boolean
  removingTagAttrs?: boolean
}) => Plugin = (options?: {}) => {
  return {
    name: "vite-svg-patch-plugin",
    transform: function (code, id) {
      if (
        id.endsWith(".svg")
      ) {
        const extractedSvg = fs.readFileSync(id, "utf8")
        return `export default '${getExtractedSVG(extractedSvg, options)}'`
      }
      return code
    }
  }
}

cslecours avatar May 21 '21 19:05 cslecours

While this is in the process of getting fixed, here is my current patchy solution (leveraging webpack's svg-inline-loader 😅 )

Add svgLoader() to your plugins array and you're good to go!

import { getExtractedSVG } from "svg-inline-loader"
import type { Plugin } from "rollup"
import fs from "fs"

//TODO: remove this once https://github.com/vitejs/vite/pull/2909 gets merged
export const svgLoader: (options?: {
  classPrefix?: string
  idPrefix?: string
  removeSVGTagAttrs?: boolean
  warnTags?: boolean
  removeTags?: boolean
  warnTagAttrs?: boolean
  removingTagAttrs?: boolean
}) => Plugin = (options?: {}) => {
  return {
    name: "vite-svg-patch-plugin",
    transform: function (code, id) {
      if (
        id.endsWith(".svg")
      ) {
        const extractedSvg = fs.readFileSync(id, "utf8")
        return `export default '${getExtractedSVG(extractedSvg, options)}'`
      }
      return code
    }
  }
}

THANKS! you are awesome!

herberthobregon avatar May 29 '21 08:05 herberthobregon

While this is in the process of getting fixed, here is my current patchy solution (leveraging webpack's svg-inline-loader )

Add svgLoader() to your plugins array and you're good to go!

import { getExtractedSVG } from "svg-inline-loader"
import type { Plugin } from "rollup"
import fs from "fs"

//TODO: remove this once https://github.com/vitejs/vite/pull/2909 gets merged
export const svgLoader: (options?: {
  classPrefix?: string
  idPrefix?: string
  removeSVGTagAttrs?: boolean
  warnTags?: boolean
  removeTags?: boolean
  warnTagAttrs?: boolean
  removingTagAttrs?: boolean
}) => Plugin = (options?: {}) => {
  return {
    name: "vite-svg-patch-plugin",
    transform: function (code, id) {
      if (
        id.endsWith(".svg")
      ) {
        const extractedSvg = fs.readFileSync(id, "utf8")
        return `export default '${getExtractedSVG(extractedSvg, options)}'`
      }
      return code
    }
  }
}

For now, this does not support svg files in styles.

hiendv avatar May 31 '21 03:05 hiendv

Hi guys, I am experimenting with package vite-svg-loader to make our SVG icons inline. Although SVG icons have already inline HTML when server-side render, the browser still downloads SVG icons through network when rehydrate. We are trying to improve page speed, so inlining small SVG with HTML instead of making requests to download them is one method that we are experimenting with.

Do you know any package like html-loader for Vite/Rollup ecosystem?

hieu-ht avatar Jul 19 '21 13:07 hieu-ht

How about something like this?

import logoSvgString from './assets/logo.svg?raw';

const fragment = document.createDocumentFragment();
const logoFragment = document
	.createRange()
	.createContextualFragment(logoSvgString);

fragment.appendChild(logoFragment);
document.body.appendChild(fragment);

Will not work for the <style> case though :/

mateatslc avatar Aug 04 '21 20:08 mateatslc

Custom SVG are now supported in unplugin-icons, which allows them to be very easily inlined (relevant discuccion https://github.com/antfu/unplugin-icons/issues/12)

You can find the documentation here: https://github.com/antfu/unplugin-icons#custom-icons

oliverpool avatar Sep 16 '21 11:09 oliverpool

I actually had to get the inlined svg to use in img src, so I adapted the plugin by @cslecours to use the same data uri extractor as in the PR; see code below if you are looking for the same solution (note the double quotes around the data uri).

import svgToMiniDataURI from "mini-svg-data-uri";
import type { Plugin } from "rollup";
import fs from "fs";
import { optimize, OptimizeOptions } from "svgo";

type PluginOptions = { noOptimize?: boolean; svgo?: OptimizeOptions };

//TODO: remove this once https://github.com/vitejs/vite/pull/2909 gets merged
export const svgLoader: (options?: PluginOptions) => Plugin = (
  options?: PluginOptions
) => {
  // these options will always be overridden
  const overrideOptions: PluginOptions = {
    svgo: {
      // set multipass to allow all optimizations
      multipass: true,
      // setting datauri to undefined will get pure svg
      // since we want to encode with mini-svg-data-uri
      datauri: undefined,
    },
  };
  options = options ?? overrideOptions;
  options.svgo = Object.assign(options.svgo ?? {}, overrideOptions.svgo);
  return {
    name: "vite-svg-patch-plugin",
    transform: function (code, id) {
      if (id.endsWith(".svg")) {
        const extractedSvg = fs.readFileSync(id, "utf8");
        const optimized = options.noOptimize
          ? extractedSvg
          : optimize(extractedSvg, options.svgo).data;
        const datauri = svgToMiniDataURI.toSrcset(optimized);
        return `export default "${datauri}"`;
      }
      return code;
    },
  };
};

(makes using dynamic import such as in this gist really powerful)

EDIT: added optional svgo optimizations

joakimriedel avatar Sep 29 '21 14:09 joakimriedel

How do you remove those assets from being emitted by Vite? Even with those plugins, the svg are still emitted as external files

tleunen avatar Jul 28 '22 18:07 tleunen

Any updates on this? Currently, I have a few websites that load multiple SVG images, which make them load pretty slowly.

ByteAtATime avatar Aug 24 '22 02:08 ByteAtATime

There's a fairly dead PR open for it, I think it mainly just needs maintainer approval at this point

madeleineostoja avatar Aug 24 '22 05:08 madeleineostoja

Does unplugin-icons solve you usecase? See https://github.com/vitejs/vite/issues/1204#issuecomment-920821983

oliverpool avatar Aug 24 '22 07:08 oliverpool

Not nearly as cleanly as just adapting the svg-inline-loader from webpack, both are hacks for a common use case

madeleineostoja avatar Aug 24 '22 12:08 madeleineostoja

How do you remove those assets from being emitted by Vite? Even with those plugins, the svg are still emitted as external files

I wrote a plugin to exclude files ending in .svg to prevent them from emitted. Add to plugin array, working as of 4.0.2

const preventSVGEmit = () => {
  return {
    generateBundle(opts, bundle) {
      for (const key in bundle) {
        if (key.endsWith('.svg')) {
          delete bundle[key]
        }
      }
    },
  }
}

Usage: plugins: [preventSVGEmit()]

eusahn avatar Dec 20 '22 00:12 eusahn

In 2023, what is the recommended solution for this? Is this in scope for vite?

nikeee avatar Apr 18 '23 22:04 nikeee

For @nikeee and anyone coming after that, it seems you can append ?inline, ?url or ?raw when importing assets. So, to get a data64 of an svg you'd go:

import myInlineSvg from './path/to/file.svg?inline';

Docs: https://vitejs.dev/guide/assets.html#explicit-url-imports

hugoatmooven avatar Apr 20 '23 05:04 hugoatmooven

@hugoatmooven i think the motivation for this issue is inlining in the sense of the SVG object that can be styled, etc etc, rather than a base64 string

madeleineostoja avatar Apr 20 '23 05:04 madeleineostoja

@hugoatmooven so inlining SVGs works now? If so, this issue could be closed, or am I getting something wrong?

nikeee avatar Apr 20 '23 07:04 nikeee

?inline means base64: <img src="data:image/svg+xml;base64,..." />.

However the goal would be to have <svg ...></svg> (to be able to style it using CSS, not possible with the img).

oliverpool avatar Apr 20 '23 07:04 oliverpool

I upgraded to the latest version 4.3.0 but even with adding ?inline the respective SVG doesn't get inlined as an data URI in my (S)CSS.

hermanndettmann avatar Apr 20 '23 09:04 hermanndettmann

@madeleineostoja @oliverpool

I think that is also available with ?raw. In a React project it would look like this:

import mySvgContent from './path/to/file.svg?raw';

function MySvgComponent() {
  return <div dangerouslySetInnerHTML={mySvgContent} />
}

hugoatmooven avatar Apr 20 '23 22:04 hugoatmooven

@hugoatmooven Well I'll be damned, ?raw works perfectly to inline raw SVG contents.

I think this issue can (finally) be closed out, or perhaps left open as an FAQ/documentation issue?

madeleineostoja avatar Apr 22 '23 03:04 madeleineostoja

@madeleineostoja While the ?raw trick is nice to have, it only works for the specific use-case where you want to embed the raw SVG directly into HTML and is using JavaScript to generate said HTML. It doesn't work in other (arguably more common) situations, such as <img src="logo.svg"/> or background-image: url(watermark.svg);, where data URIs are necessary. And you can't just do "data:image/svg+xml," + mySvgContent because of URL encoding.

Also preferably this should just work out-of-the-box, like how inlining works for every(?) other format.

andylizi avatar Apr 22 '23 04:04 andylizi

i think the motivation for this issue is inlining in the sense of the SVG object that can be styled, etc etc, rather than a base64 string

However the goal would be to have <svg ...></svg> (to be able to style it using CSS, not possible with the img).

Ah apologies I didn't notice the discussion regarding goals and motivation before.

It'd be great to be able to embed the SVG element into HTML, but that feature feels more like a future expansion to me, rather than the solution to the current problem described in this issue. I feel this way because:

  1. Sometimes it is not the desireable behavior. For example, it'd be pretty surprising if <img class="my-logo" src="logo.svg"/> gets turned into <svg class="my-logo">...</svg> silently and irrevocably. And as I mentioned before, some use-cases can only use data URIs.
  2. AFAIU, this would need to be implemented in a completely separate way compared to the current asset inlining logic, since it involves special HTML transformation.
  3. As there're different use-cases, there's no reason we can only have one way of inlining SVGs, and any future implementation of such won't (and shouldn't) conflict with data URIs. The pros and cons of adding that feature, especially the question of whether it was in scope for vite (instead of, like, a plugin), probably need to happen in another discussion.

andylizi avatar Apr 22 '23 04:04 andylizi

?inline means base64: <img src="data:image/svg+xml;base64,..." />.

However the goal would be to have <svg ...></svg> (to be able to style it using CSS, not possible with the img).

It seems this could be done by appending ?raw&inline to the SVG file path:

import MyLogo from 'path/to/svg?raw&inline';

MyLogo will be <svg ...></svg>. Then you can embed it directly in HTML (Svelte):

{@html MyLogo}

Or if you want to use it for a favicon or <img>:

<link rel="icon" href="data:image/svg+xml;base64,{btoa(MyLogo)}" type="image/svg+xml" />
<img src="data:image/svg+xml;base64,{btoa(MyLogo)}" alt="" />

Here’s my code: https://github.com/sveltia/sveltia-cms/commit/4dc8c63ec99acdbd4a68cbabbc2a225ce9453a3a All these files are bundled into one single JavaScript file sveltia-cms.js.

kyoshino avatar May 10 '23 23:05 kyoshino

?inline means base64: <img src="data:image/svg+xml;base64,..." />.

However the goal would be to have <svg ...></svg> (to be able to style it using CSS, not possible with the img).

Doing ?inline should emit <img src="data:image/svg+xml;base64,..." /> or <img src="data:image/svg+xml;utf-8,..." /> as per docs. But currently, this doesn't and is broken. It emits <img src="/path/to/image.svg?inline" /> instead. Due to the SVG not being inlined, a seperate network request ist done, creating a visible lag.

I think vite should not do magic and emit <svg>...</svg>, as this has entirely different semantics and would break stuff.

Also, this approach doesn't work for imports in CSS files (which is what I am using this for):

.a {
    background-image: url("./path/to/image.svg?inline");
    /*
    expected result:
    background-image: url("data:image/svg+xml;base64,...");

    actual result:
    background-image: url("/path/to/image-hash.svg?inline");
    */
}

This is currently broken, too. Probably the logic between the include implementations is shared.

nikeee avatar May 10 '23 23:05 nikeee

as a workaround for inlining SVGs in CSS files i'm now using the postcss-inline-svg plugin.

oberhamsi avatar Jun 01 '23 11:06 oberhamsi

Thanks @oberhamsi for that suggestion! I'll use it now too!

hermanndettmann avatar Jun 12 '23 11:06 hermanndettmann

as a workaround for inlining SVGs in CSS files i'm now using the postcss-inline-svg plugin.

This is so great! Thank you for this. Out of the box support for PostCSS in Vite is amazing as well. So I added "postcss-inline-svg" as dev dependency, and then created a "postcss.config.cjs" config file ( I had to use .cjs extension) that references the plugin with the usual syntax, for example:

module.exports = {
  plugins: {
    'postcss-inline-svg': {}
  }
}

Then in CSS I load svgs like

background-image: svg-load('./assets/vite.svg');

When built, all the svg are automatically inlined and are not outputted to the dist. Finally!

micscala avatar Oct 12 '23 09:10 micscala

@madeleineostoja While the ?raw trick is nice to have, it only works for the specific use-case where you want to embed the raw SVG directly into HTML and is using JavaScript to generate said HTML. It doesn't work in other (arguably more common) situations, such as <img src="logo.svg"/> or background-image: url(watermark.svg);, where data URIs are necessary. And you can't just do "data:image/svg+xml," + mySvgContent because of URL encoding.

Also preferably this should just work out-of-the-box, like how inlining works for every(?) other format.

For automatic inlining in CSS, see my previous reply. For img src inlining, here is how I do:

  • Load with ?raw, for example:

import javascriptLogo from './assets/javascript.svg?raw'

  • add a JS function to the main.js:
const svg = (() => {
  // Source: https://github.com/tigt/mini-svg-data-uri
  // see: https://github.com/tigt/mini-svg-data-uri/issues/24
  const reWhitespace = /\s+/g
  const reUrlHexPairs = /%[\dA-F]{2}/g
  const hexDecode = { '%20': ' ', '%3D': '=', '%3A': ':', '%2F': '/' }
  const specialHexDecode = match => hexDecode[match] || match.toLowerCase()
  const svgToTinyDataUri = svg => {
    svg = String(svg)
    if (svg.charCodeAt(0) === 0xfeff) svg = svg.slice(1)
    svg = svg.trim().replace(reWhitespace, ' ').replaceAll('"', '\'')
    svg = encodeURIComponent(svg)
    svg = svg.replace(reUrlHexPairs, specialHexDecode)
    return 'data:image/svg+xml,' + svg
  }
  svgToTinyDataUri.toSrcset = svg => svgToTinyDataUri(svg).replace(/ /g, '%20')
  return svgToTinyDataUri
})()

then in the app, when you need to inline a svg in a img src load it with:

<img src="${svg(javascriptLogo)}" class="logo vanilla" alt="JavaScript logo" />

micscala avatar Oct 12 '23 09:10 micscala