From 0f17a74c64a6664c68f23c92d572c22d1a4de059 Mon Sep 17 00:00:00 2001 From: HcySunYang Date: Wed, 24 Feb 2021 23:53:30 +0800 Subject: [PATCH] fix: should transform the img tag's srcset arrtibute and css' image-set property (#2188) fix #2177 --- .../assets/__tests__/assets.spec.ts | 28 ++++++++++ packages/playground/assets/css/css-url.css | 16 ++++++ packages/playground/assets/index.html | 20 +++++++ packages/vite/src/node/plugins/css.ts | 54 ++++++++++++++----- packages/vite/src/node/plugins/html.ts | 13 +++-- packages/vite/src/node/utils.ts | 35 ++++++++++++ 6 files changed, 149 insertions(+), 17 deletions(-) diff --git a/packages/playground/assets/__tests__/assets.spec.ts b/packages/playground/assets/__tests__/assets.spec.ts index e5f7c6af1ff777..621d738133ee8d 100644 --- a/packages/playground/assets/__tests__/assets.spec.ts +++ b/packages/playground/assets/__tests__/assets.spec.ts @@ -81,6 +81,20 @@ describe('css url() references', () => { expect(await getBg('.css-url-relative')).toMatch(assetMatch) }) + test('image-set relative', async () => { + let imageSet = await getBg('.css-image-set-relative') + imageSet.split(', ').forEach((s) => { + expect(s).toMatch(assetMatch) + }) + }) + + test('image-set without the url() call', async () => { + let imageSet = await getBg('.css-image-set-without-url-call') + imageSet.split(', ').forEach((s) => { + expect(s).toMatch(assetMatch) + }) + }) + test('relative in @import', async () => { expect(await getBg('.css-url-relative-at-imported')).toMatch(assetMatch) }) @@ -117,6 +131,20 @@ describe('css url() references', () => { } }) +describe('image', () => { + test('srcset', async () => { + const img = await page.$('.img-src-set') + const srcset = await img.getAttribute('srcset') + srcset.split(', ').forEach((s) => { + expect(s).toMatch( + isBuild + ? /\/foo\/assets\/asset\.\w{8}\.png \d{1}x/ + : /\.\/nested\/asset\.png \d{1}x/ + ) + }) + }) +}) + describe('svg fragments', () => { // 404 is checked already, so here we just ensure the urls end with #fragment test('img url', async () => { diff --git a/packages/playground/assets/css/css-url.css b/packages/playground/assets/css/css-url.css index ae2dff3f633405..8a3f00dee17bd9 100644 --- a/packages/playground/assets/css/css-url.css +++ b/packages/playground/assets/css/css-url.css @@ -10,6 +10,22 @@ background-size: 10px; } +.css-image-set-relative { + background-image: -webkit-image-set( + url('../nested/asset.png') 1x, + url('../nested/asset.png') 2x + ); + background-size: 10px; +} + +.css-image-set-without-url-call { + background-image: -webkit-image-set( + '../nested/asset.png' 1x, + '../nested/asset.png' 2x + ); + background-size: 10px; +} + .css-url-public { background: url('/icon.png'); background-size: 10px; diff --git a/packages/playground/assets/index.html b/packages/playground/assets/index.html index 1819dcf2b69a21..cc0b8fd33c8250 100644 --- a/packages/playground/assets/index.html +++ b/packages/playground/assets/index.html @@ -30,6 +30,16 @@

CSS url references

CSS background (relative)
+
+ CSS background with image-set() (relative) +
+
+ CSS background with image-set() (relative) +
CSS background (relative from @imported file in different dir)CSS url references CSS background (aliased)
+

Image Src Set

+
+ +
+

SVG Fragments

string | Promise const cssUrlRE = /url\(\s*('[^']+'|"[^"]+"|[^'")]+)\s*\)/ +const cssImageSetRE = /image-set\(([^)]+)\)/ const UrlRewritePostcssPlugin: Postcss.PluginCreator<{ replacer: CssUrlReplacer @@ -711,13 +713,16 @@ const UrlRewritePostcssPlugin: Postcss.PluginCreator<{ Once(root) { const promises: Promise[] = [] root.walkDecls((decl) => { - if (cssUrlRE.test(decl.value)) { + const isCssUrl = cssUrlRE.test(decl.value) + const isCssImageSet = cssImageSetRE.test(decl.value) + if (isCssUrl || isCssImageSet) { const replacerForDecl = (rawUrl: string) => { const importer = decl.source?.input.file return opts.replacer(rawUrl, importer) } + const rewriterToUse = isCssUrl ? rewriteCssUrls : rewriteCssImageSet promises.push( - rewriteCssUrls(decl.value, replacerForDecl).then((url) => { + rewriterToUse(decl.value, replacerForDecl).then((url) => { decl.value = url }) ) @@ -737,19 +742,40 @@ function rewriteCssUrls( ): Promise { return asyncReplace(css, cssUrlRE, async (match) => { let [matched, rawUrl] = match - let wrap = '' - const first = rawUrl[0] - if (first === `"` || first === `'`) { - wrap = first - rawUrl = rawUrl.slice(1, -1) - } - if (isExternalUrl(rawUrl) || isDataUrl(rawUrl) || rawUrl.startsWith('#')) { - return matched - } - return `url(${wrap}${await replacer(rawUrl)}${wrap})` + return await doUrlReplace(rawUrl, matched, replacer) }) } +function rewriteCssImageSet( + css: string, + replacer: CssUrlReplacer +): Promise { + return asyncReplace(css, cssImageSetRE, async (match) => { + let [matched, rawUrl] = match + const url = await processSrcSet(rawUrl, ({ url }) => + doUrlReplace(url, matched, replacer) + ) + return `image-set(${url})` + }) +} +async function doUrlReplace( + rawUrl: string, + matched: string, + replacer: CssUrlReplacer +) { + let wrap = '' + const first = rawUrl[0] + if (first === `"` || first === `'`) { + wrap = first + rawUrl = rawUrl.slice(1, -1) + } + if (isExternalUrl(rawUrl) || isDataUrl(rawUrl) || rawUrl.startsWith('#')) { + return matched + } + + return `url(${wrap}${await replacer(rawUrl)}${wrap})` +} + let CleanCSS: any async function minifyCSS(css: string, config: ResolvedConfig) { diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index a523017e97dbc6..e4c97a1c5c9c78 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -8,7 +8,8 @@ import { cleanUrl, isExternalUrl, isDataUrl, - generateCodeFrame + generateCodeFrame, + processSrcSet } from '../utils' import { ResolvedConfig } from '../config' import MagicString from 'magic-string' @@ -69,7 +70,7 @@ export const assetAttrsConfig: Record = { link: ['href'], video: ['src', 'poster'], source: ['src'], - img: ['src'], + img: ['src', 'srcset'], image: ['xlink:href', 'href'], use: ['xlink:href', 'href'] } @@ -233,7 +234,13 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { for (const attr of assetUrls) { const value = attr.value! try { - const url = await urlToBuiltUrl(value.content, id, config, this) + const url = + attr.name === 'srcset' + ? await processSrcSet(value.content, ({ url }) => + urlToBuiltUrl(url, id, config, this) + ) + : await urlToBuiltUrl(value.content, id, config, this) + s.overwrite( value.loc.start.offset, value.loc.end.offset, diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index d2f6810c077e0a..5cfe8146657620 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -352,3 +352,38 @@ export function ensureWatchedFile( watcher.add(path.resolve(file)) } } + +interface ImageCandidate { + url: string + descriptor: string +} +const escapedSpaceCharacters = /( |\\t|\\n|\\f|\\r)+/g +export async function processSrcSet( + srcs: string, + replacer: (arg: ImageCandidate) => Promise +) { + const imageCandidates: ImageCandidate[] = srcs.split(',').map((s) => { + const [url, descriptor] = s + .replace(escapedSpaceCharacters, ' ') + .trim() + .split(' ', 2) + return { url, descriptor } + }) + + const ret = await Promise.all( + imageCandidates.map(async ({ url, descriptor }) => { + return { + url: await replacer({ url, descriptor }), + descriptor + } + }) + ) + + const url = ret.reduce((prev, { url, descriptor }, index) => { + descriptor = descriptor || '' + return (prev += + url + ` ${descriptor}${index === ret.length - 1 ? '' : ', '}`) + }, '') + + return url +}