vitejs / vite

Next generation frontend tooling. It's fast!
http://vitejs.dev
MIT License
67.21k stars 6.04k forks source link

Support inlining SVG assets #1204

Closed andylizi closed 11 months ago

andylizi commented 3 years ago

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

Actual behavior

System Info

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.

cslecours commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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

joakimriedel commented 2 years ago

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 commented 2 years ago

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

ByteAtATime commented 2 years ago

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

madeleineostoja commented 2 years ago

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

oliverpool commented 2 years ago

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

madeleineostoja commented 2 years ago

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

eusahn commented 1 year ago

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 commented 1 year ago

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

hugoatmooven commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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

oliverpool commented 1 year ago

?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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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

nikeee commented 1 year ago

?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 commented 1 year ago

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

hermanndettmann commented 1 year ago

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

micscala commented 11 months ago

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 commented 11 months ago

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

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

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