Skip to content

Commit

Permalink
fix(css): fix url rewriting in @imported css
Browse files Browse the repository at this point in the history
fix #1629
  • Loading branch information
yyx990803 committed Jan 22, 2021
1 parent cf81aa3 commit 52ae44f
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 29 deletions.
4 changes: 4 additions & 0 deletions packages/playground/assets/__tests__/assets.spec.ts
Expand Up @@ -57,6 +57,10 @@ describe('css url() references', () => {
expect(await getBg('.css-url-relative')).toMatch(assetMatch)
})

test('relative in @import', async () => {
expect(await getBg('.css-url-relative-at-imported')).toMatch(assetMatch)
})

test('absolute', async () => {
expect(await getBg('.css-url-absolute')).toMatch(assetMatch)
})
Expand Down
4 changes: 3 additions & 1 deletion packages/playground/assets/css/css-url.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/playground/assets/css/nested/at-imported-css-url.css
@@ -0,0 +1,4 @@
.css-url-relative-at-imported {
background: url(../../nested/asset.png);
background-size: 10px;
}
5 changes: 5 additions & 0 deletions packages/playground/assets/index.html
Expand Up @@ -28,6 +28,11 @@ <h2>CSS url references</h2>
<div class="css-url-relative">
<span style="background: #fff">CSS background (relative)</span>
</div>
<div class="css-url-relative-at-imported">
<span style="background: #fff"
>CSS background (relative from @imported file in different dir)</span
>
</div>
<div class="css-url-public">
<span style="background: #fff">CSS background (public)</span>
</div>
Expand Down
114 changes: 86 additions & 28 deletions packages/vite/src/node/plugins/css.ts
Expand Up @@ -5,7 +5,8 @@ import {
cleanUrl,
generateCodeFrame,
isDataUrl,
isObject
isObject,
normalizePath
} from '../utils'
import path from 'path'
import { Plugin } from '../plugin'
Expand All @@ -20,8 +21,13 @@ import {
} from 'rollup'
import { dataToEsm } from '@rollup/pluginutils'
import chalk from 'chalk'
import { CLIENT_PUBLIC_PATH } from '../constants'
import { ProcessOptions, Result, Plugin as PostcssPlugin } from 'postcss'
import { CLIENT_PUBLIC_PATH, FS_PREFIX } from '../constants'
import {
ProcessOptions,
Result,
Plugin as PostcssPlugin,
PluginCreator
} from 'postcss'
import { ViteDevServer } from '../'
import { assetUrlRE, urlToBuiltUrl } from './asset'
import MagicString from 'magic-string'
Expand Down Expand Up @@ -92,15 +98,38 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
return
}

let { code: css, modules, deps } = await compileCSS(id, raw, config)
const urlReplacer: CssUrlReplacer = server
? (url, importer) => {
if (url.startsWith('/')) return url
const filePath = normalizePath(
path.resolve(path.dirname(importer || id), url)
)
if (filePath.startsWith(config.root)) {
return filePath.slice(config.root.length)
} else {
return `${FS_PREFIX}${filePath}`
}
}
: (url, importer) => {
return urlToBuiltUrl(url, importer || id, config, this)
}

const { code: css, modules, deps } = await compileCSS(
id,
raw,
config,
urlReplacer
)
if (modules) {
moduleCache.set(id, modules)
}

// dev
if (server) {
// server only logic for handling CSS @import dependency hmr
const { moduleGraph } = server
const thisModule = moduleGraph.getModuleById(id)!

// CSS modules cannot self-accept since it exports values
const isSelfAccepting = !modules
if (deps) {
Expand All @@ -123,18 +152,8 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
} else {
thisModule.isSelfAccepting = isSelfAccepting
}
// rewrite urls using current module's url as base
css = await rewriteCssUrls(css, thisModule.url)
} else {
// if build, analyze url() asset reference
// account for comments https://github.com/vitejs/vite/issues/426
css = css.replace(/\/\*[\s\S]*?\*\//gm, '')
if (cssUrlRE.test(css)) {
css = await rewriteCssUrls(css, async (url) =>
urlToBuiltUrl(url, id, config, this)
)
}
}

return css
}
}
Expand Down Expand Up @@ -304,7 +323,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
async function compileCSS(
id: string,
code: string,
config: ResolvedConfig
config: ResolvedConfig,
urlReplacer: CssUrlReplacer
): Promise<{
code: string
map?: SourceMap
Expand All @@ -317,11 +337,18 @@ 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 postcssConfig = await resolvePostcssConfig(config)
const lang = id.match(cssLangRE)?.[1]

// 1. plain css that needs no processing
if (lang === 'css' && !postcssConfig && !isModule && !needInlineImport) {
if (
lang === 'css' &&
!postcssConfig &&
!isModule &&
!needInlineImport &&
!hasUrl
) {
return { code }
}

Expand Down Expand Up @@ -377,6 +404,11 @@ async function compileCSS(
if (needInlineImport) {
postcssPlugins.unshift((await import('postcss-import')).default())
}
postcssPlugins.push(
UrlRewritePostcssPlugin({
replacer: urlReplacer
}) as PostcssPlugin
)

if (isModule) {
postcssPlugins.unshift(
Expand Down Expand Up @@ -645,22 +677,48 @@ const preProcessors = {
stylus: styl
}

type Replacer = (url: string) => string | Promise<string>
type CssUrlReplacer = (
url: string,
importer?: string
) => string | Promise<string>
const cssUrlRE = /url\(\s*('[^']+'|"[^"]+"|[^'")]+)\s*\)/

function rewriteCssUrls(
css: string,
replacerOrBase: string | Replacer
): Promise<string> {
let replacer: Replacer
if (typeof replacerOrBase === 'string') {
replacer = (rawUrl) => {
return path.posix.resolve(path.posix.dirname(replacerOrBase), rawUrl)
const UrlRewritePostcssPlugin: PluginCreator<{
replacer: CssUrlReplacer
}> = (opts) => {
if (!opts) {
throw new Error('base or replace is required')
}

return {
postcssPlugin: 'vite-url-rewrite',
Once(root) {
const promises: Promise<void>[] = []
root.walkDecls((decl) => {
if (cssUrlRE.test(decl.value)) {
const replacerForDecl = (rawUrl: string) => {
const importer = decl.source?.input.file
return opts.replacer(rawUrl, importer)
}
promises.push(
rewriteCssUrls(decl.value, replacerForDecl).then((url) => {
decl.value = url
})
)
}
})
if (promises.length) {
return Promise.all(promises) as any
}
}
} else {
replacer = replacerOrBase
}
}
UrlRewritePostcssPlugin.postcss = true

function rewriteCssUrls(
css: string,
replacer: CssUrlReplacer
): Promise<string> {
return asyncReplace(css, cssUrlRE, async (match) => {
let [matched, rawUrl] = match
let wrap = ''
Expand Down

0 comments on commit 52ae44f

Please sign in to comment.