From 685bdf7a3c3ac3188aced6477edbe174724d559f Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Mon, 25 Jan 2021 14:22:52 -0500 Subject: [PATCH 1/4] feat: allow .svg files to be inlined Base64 is unnecessary for SVG files: https://github.com/vitejs/vite/issues/1197\#issuecomment-738780169 Also using `Buffer.byteLength` instead of character length when comparing with the `assetsInlineLimit` option. Closes #1204 --- packages/vite/src/node/plugins/asset.ts | 43 ++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index f6bee942f5caae..5abd8e8ee1d168 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -208,11 +208,13 @@ async function fileToBuiltUrl( let url if ( config.build.lib || - (!file.endsWith('.svg') && - content.length < Number(config.build.assetsInlineLimit)) + Buffer.byteLength(content) < config.build.assetsInlineLimit ) { - // base64 inlined as a string - url = `data:${mime.getType(file)};base64,${content.toString('base64')}` + // svgs can be inlined without base64 + url = file.endsWith('.svg') + ? `data:image/svg+xml;utf8,${uriEncodeSvg(content.toString('utf-8'))}` + : // base64 inlined as a string + `data:${mime.getType(file)};base64,${content.toString('base64')}` } else { // emit as asset // rollup supports `import.meta.ROLLUP_FILE_URL_*`, but it generates code @@ -250,6 +252,39 @@ async function fileToBuiltUrl( return url } +function specialHexEncode(match: string) { + // Browsers tolerate these characters, and they're frequent + switch (match) { + case '%20': + return ' ' + case '%3D': + return '=' + case '%3A': + return ':' + case '%2F': + return '/' + default: + return match.toLowerCase() // compresses better + } +} + +const doubleQuoteRE = /"/g +const whitespaceRE = /\s+/g +const urlHexPairsRE = /%[\dA-F]{2}/g + +// Adopted from https://github.com/tigt/mini-svg-data-uri +function uriEncodeSvg(content: string) { + const normalizedContent = content + .trim() + .replace(whitespaceRE, ' ') + .replace(doubleQuoteRE, "'") + + return encodeURIComponent(normalizedContent).replace( + urlHexPairsRE, + specialHexEncode + ) +} + function getAssetHash(content: Buffer) { return createHash('sha256').update(content).digest('hex').slice(0, 8) } From 1bb658f3c21f888f5f61bc4c6d570748b6f4086c Mon Sep 17 00:00:00 2001 From: Eugene Datsky Date: Tue, 18 May 2021 09:43:41 +1000 Subject: [PATCH 2/4] fix: support fragments --- packages/playground/assets/__tests__/assets.spec.ts | 2 +- packages/vite/src/node/plugins/asset.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/playground/assets/__tests__/assets.spec.ts b/packages/playground/assets/__tests__/assets.spec.ts index 045c4c8f827191..ac8698a762a15d 100644 --- a/packages/playground/assets/__tests__/assets.spec.ts +++ b/packages/playground/assets/__tests__/assets.spec.ts @@ -189,7 +189,7 @@ if (isBuild) { for (const file of listAssets('foo')) { if (file.endsWith('.css')) { expect(entry.css).toContain(`assets/${file}`) - } else if (!file.endsWith('.js')) { + } else if (!file.endsWith('.js') && !file.endsWith('.svg')) { expect(entry.assets).toContain(`assets/${file}`) } } diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 5abd8e8ee1d168..fc3b44d80b6bd0 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -207,8 +207,9 @@ async function fileToBuiltUrl( let url if ( - config.build.lib || - Buffer.byteLength(content) < config.build.assetsInlineLimit + (config.build.lib || + Buffer.byteLength(content) < config.build.assetsInlineLimit) && + hash == null ) { // svgs can be inlined without base64 url = file.endsWith('.svg') From 0d03f5f72ecd23db208a1c15c9fc578b733ea40c Mon Sep 17 00:00:00 2001 From: Eugene Datsky Date: Tue, 18 May 2021 09:44:09 +1000 Subject: [PATCH 3/4] fix: review feedback --- packages/vite/LICENSE.md | 29 ++++++++++++++++++ packages/vite/package.json | 1 + packages/vite/src/node/plugins/asset.ts | 39 ++++--------------------- yarn.lock | 5 ++++ 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/packages/vite/LICENSE.md b/packages/vite/LICENSE.md index e76cc51ddbc553..9fb16ec3079bbc 100644 --- a/packages/vite/LICENSE.md +++ b/packages/vite/LICENSE.md @@ -3076,6 +3076,35 @@ Repository: sindresorhus/mimic-fn --------------------------------------- +## mini-svg-data-uri +License: MIT +By: Taylor “Tigt” Hunt +Repository: git+https://github.com/tigt/mini-svg-data-uri.git + +> MIT License +> +> Copyright (c) 2018 Taylor Hunt +> +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all +> copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +> SOFTWARE. + +--------------------------------------- + ## minimatch License: ISC By: Isaac Z. Schlueter diff --git a/packages/vite/package.json b/packages/vite/package.json index e7f918f8a312e3..ead4030667efbf 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -105,6 +105,7 @@ "launch-editor-middleware": "^2.2.1", "magic-string": "^0.25.7", "mime": "^2.4.7", + "mini-svg-data-uri": "^1.3.3", "minimatch": "^3.0.4", "okie": "^1.0.1", "open": "^7.4.2", diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index fc3b44d80b6bd0..9ffccefd9d7091 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -8,6 +8,7 @@ import { cleanUrl } from '../utils' import { FS_PREFIX } from '../constants' import { PluginContext, RenderedChunk } from 'rollup' import MagicString from 'magic-string' +import svgToTinyDataUri from 'mini-svg-data-uri' import { createHash } from 'crypto' export const assetUrlRE = /__VITE_ASSET__([a-z\d]{8})__(?:\$_(.*?)__)?/g @@ -213,7 +214,10 @@ async function fileToBuiltUrl( ) { // svgs can be inlined without base64 url = file.endsWith('.svg') - ? `data:image/svg+xml;utf8,${uriEncodeSvg(content.toString('utf-8'))}` + ? // The only difference between the default method and `toSrcset` is that + // the latter encodes spaces as `%20`, so it's safer to always use it + // to support `srcset` use-case even when svg is imported into JavaScript + svgToTinyDataUri.toSrcset(content.toString()) : // base64 inlined as a string `data:${mime.getType(file)};base64,${content.toString('base64')}` } else { @@ -253,39 +257,6 @@ async function fileToBuiltUrl( return url } -function specialHexEncode(match: string) { - // Browsers tolerate these characters, and they're frequent - switch (match) { - case '%20': - return ' ' - case '%3D': - return '=' - case '%3A': - return ':' - case '%2F': - return '/' - default: - return match.toLowerCase() // compresses better - } -} - -const doubleQuoteRE = /"/g -const whitespaceRE = /\s+/g -const urlHexPairsRE = /%[\dA-F]{2}/g - -// Adopted from https://github.com/tigt/mini-svg-data-uri -function uriEncodeSvg(content: string) { - const normalizedContent = content - .trim() - .replace(whitespaceRE, ' ') - .replace(doubleQuoteRE, "'") - - return encodeURIComponent(normalizedContent).replace( - urlHexPairsRE, - specialHexEncode - ) -} - function getAssetHash(content: Buffer) { return createHash('sha256').update(content).digest('hex').slice(0, 8) } diff --git a/yarn.lock b/yarn.lock index fb632e8fda72ba..ada2b778b73f7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5447,6 +5447,11 @@ mini-create-react-context@^0.4.0: "@babel/runtime" "^7.12.1" tiny-warning "^1.0.3" +mini-svg-data-uri@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.3.3.tgz#91d2c09f45e056e5e1043340b8b37ba7b50f4fac" + integrity sha512-+fA2oRcR1dJI/7ITmeQJDrYWks0wodlOz0pAEhKYJ2IVc1z0AnwJUsKY2fzFmPAM3Jo9J0rBx8JAA9QQSJ5PuA== + minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" From d62b1486421f71c8b80e37a657d1d5630a1323e5 Mon Sep 17 00:00:00 2001 From: Eugene Datsky Date: Tue, 18 May 2021 15:48:22 +1000 Subject: [PATCH 4/4] fix: support built tests --- packages/playground/assets/__tests__/assets.spec.ts | 4 +++- packages/playground/vue/__tests__/vue.spec.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/playground/assets/__tests__/assets.spec.ts b/packages/playground/assets/__tests__/assets.spec.ts index ac8698a762a15d..f18546ccced20c 100644 --- a/packages/playground/assets/__tests__/assets.spec.ts +++ b/packages/playground/assets/__tests__/assets.spec.ts @@ -162,7 +162,9 @@ describe('svg fragments', () => { test('from js import', async () => { const img = await page.$('.svg-frag-import') - expect(await img.getAttribute('src')).toMatch(/svg#icon-heart-view$/) + expect(await img.getAttribute('src')).toMatch( + isBuild ? /svg%3e#icon-heart-view$/ : /svg#icon-heart-view$/ + ) }) }) diff --git a/packages/playground/vue/__tests__/vue.spec.ts b/packages/playground/vue/__tests__/vue.spec.ts index bc5d4e3a5ca9f3..acc752d54151b5 100644 --- a/packages/playground/vue/__tests__/vue.spec.ts +++ b/packages/playground/vue/__tests__/vue.spec.ts @@ -110,7 +110,9 @@ describe('asset reference', () => { test('svg fragment', async () => { const img = await page.$('.svg-frag') - expect(await img.getAttribute('src')).toMatch(/svg#icon-heart-view$/) + expect(await img.getAttribute('src')).toMatch( + isBuild ? /svg%3e#icon-heart-view$/ : /svg#icon-heart-view$/ + ) }) test('relative url from