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

fix: should transform the img tag's srcset arrtibute and css' image-set property #2188

Merged
merged 3 commits into from
Feb 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/playground/assets/__tests__/assets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down Expand Up @@ -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 () => {
Expand Down
16 changes: 16 additions & 0 deletions packages/playground/assets/css/css-url.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions packages/playground/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ <h2>CSS url references</h2>
<div class="css-url-relative">
<span style="background: #fff">CSS background (relative)</span>
</div>
<div class="css-image-set-relative">
<span style="background: #fff"
>CSS background with image-set() (relative)</span
>
</div>
<div class="css-image-set-without-url-call">
<span style="background: #fff"
>CSS background with image-set() (relative)</span
>
</div>
<div class="css-url-relative-at-imported">
<span style="background: #fff"
>CSS background (relative from @imported file in different dir)</span
Expand All @@ -56,6 +66,16 @@ <h2>CSS url references</h2>
<span style="background: #fff">CSS background (aliased)</span>
</div>

<h2>Image Src Set</h2>
<div>
<img
class="img-src-set"
src="./nested/asset.png"
srcset="./nested/asset.png 1x, ./nested/asset.png 2x"
alt=""
/>
</div>

<h2>SVG Fragments</h2>
<div>
<img
Expand Down
54 changes: 40 additions & 14 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
generateCodeFrame,
isDataUrl,
isObject,
normalizePath
normalizePath,
processSrcSet
} from '../utils'
import { Plugin } from '../plugin'
import { ResolvedConfig } from '../config'
Expand Down Expand Up @@ -500,7 +501,7 @@ async function compileCSS(
// although at serve time it can work without processing, we do need to
// crawl them in order to register watch dependencies.
const needInlineImport = code.includes('@import')
const hasUrl = cssUrlRE.test(code)
const hasUrl = cssUrlRE.test(code) || cssImageSetRE.test(code)
const postcssConfig = await resolvePostcssConfig(config)
const lang = id.match(cssLangRE)?.[1]

Expand Down Expand Up @@ -698,6 +699,7 @@ type CssUrlReplacer = (
importer?: string
) => string | Promise<string>
const cssUrlRE = /url\(\s*('[^']+'|"[^"]+"|[^'")]+)\s*\)/
const cssImageSetRE = /image-set\(([^)]+)\)/

const UrlRewritePostcssPlugin: Postcss.PluginCreator<{
replacer: CssUrlReplacer
Expand All @@ -711,13 +713,16 @@ const UrlRewritePostcssPlugin: Postcss.PluginCreator<{
Once(root) {
const promises: Promise<void>[] = []
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
})
)
Expand All @@ -737,19 +742,40 @@ function rewriteCssUrls(
): Promise<string> {
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<string> {
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) {
Expand Down
13 changes: 10 additions & 3 deletions packages/vite/src/node/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
cleanUrl,
isExternalUrl,
isDataUrl,
generateCodeFrame
generateCodeFrame,
processSrcSet
} from '../utils'
import { ResolvedConfig } from '../config'
import MagicString from 'magic-string'
Expand Down Expand Up @@ -69,7 +70,7 @@ export const assetAttrsConfig: Record<string, string[]> = {
link: ['href'],
video: ['src', 'poster'],
source: ['src'],
img: ['src'],
img: ['src', 'srcset'],
image: ['xlink:href', 'href'],
use: ['xlink:href', 'href']
}
Expand Down Expand Up @@ -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,
Expand Down
35 changes: 35 additions & 0 deletions packages/vite/src/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>
) {
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
}