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

Support inlining SVG assets #1204

Closed
andylizi opened this issue Dec 5, 2020 · 29 comments · Fixed by #14643
Closed

Support inlining SVG assets #1204

andylizi opened this issue Dec 5, 2020 · 29 comments · Fixed by #14643
Labels
enhancement New feature or request

Comments

@andylizi
Copy link
Contributor

andylizi commented Dec 5, 2020

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.

    vite/src/node/config.ts

    Lines 315 to 320 in 9904cb1

    /**
    * Static asset files smaller than this number (in bytes) will be inlined as
    * base64 strings. Default limit is `4096` (4kb). Set to `0` to disable.
    * @default 4096
    */
    assetsInlineLimit: number

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

if (!id.endsWith(`.svg`) && content.length < Number(inlineLimit)) {
url = `data:${mime.lookup(id)};base64,${content.toString('base64')}`
content = undefined
}

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.

@cslecours
Copy link

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
    }
  }
}

@herberthobregon
Copy link

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!

@hiendv
Copy link

hiendv commented May 31, 2021

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.

@hieu-ht
Copy link

hieu-ht commented Jul 19, 2021

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?

@mateatslc
Copy link

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 :/

@oliverpool
Copy link
Contributor

Custom SVG are now supported in unplugin-icons, which allows them to be very easily inlined (relevant discuccion unplugin/unplugin-icons#12)

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

@joakimriedel
Copy link

joakimriedel commented Sep 29, 2021

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

@tleunen
Copy link

tleunen commented Jul 28, 2022

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

@ByteAtATime
Copy link

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

@madeleineostoja
Copy link

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

@oliverpool
Copy link
Contributor

Does unplugin-icons solve you usecase?
See #1204 (comment)

@madeleineostoja
Copy link

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

@eusahn
Copy link

eusahn commented Dec 20, 2022

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()]

@nikeee
Copy link

nikeee commented Apr 18, 2023

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

@hugoatmooven
Copy link

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

@madeleineostoja
Copy link

@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

@nikeee
Copy link

nikeee commented Apr 20, 2023

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

@oliverpool
Copy link
Contributor

oliverpool commented Apr 20, 2023

?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).

@hermanndettmann
Copy link

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.

@hugoatmooven
Copy link

hugoatmooven commented Apr 20, 2023

@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} />
}

@madeleineostoja
Copy link

@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?

@andylizi
Copy link
Contributor Author

@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
Copy link
Contributor Author

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.

@kyoshino
Copy link

kyoshino commented May 10, 2023

?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: sveltia/sveltia-cms@4dc8c63 All these files are bundled into one single JavaScript file sveltia-cms.js.

@nikeee
Copy link

nikeee commented May 10, 2023

?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.

@oberhamsi
Copy link

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

@hermanndettmann
Copy link

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

@micscala
Copy link

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
Copy link

@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" />

@sapphi-red sapphi-red linked a pull request Oct 17, 2023 that will close this issue
@github-actions github-actions bot locked and limited conversation to collaborators Nov 3, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet