fix(vite): handle runtime paths in inlined styles (#27327)

This commit is contained in:
Daniel Roe 2024-05-23 15:34:06 +01:00 committed by GitHub
parent 6a271e8a56
commit c054ca084c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 53 additions and 33 deletions

View File

@ -3,10 +3,11 @@ import { useNitro } from '@nuxt/kit'
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import { withLeadingSlash, withTrailingSlash } from 'ufo' import { withLeadingSlash, withTrailingSlash } from 'ufo'
import { dirname, relative } from 'pathe' import { dirname, relative } from 'pathe'
import MagicString from 'magic-string'
const PREFIX = 'virtual:public?' const PREFIX = 'virtual:public?'
export const VitePublicDirsPlugin = createUnplugin(() => { export const VitePublicDirsPlugin = createUnplugin((options: { sourcemap?: boolean }) => {
const nitro = useNitro() const nitro = useNitro()
function resolveFromPublicAssets (id: string) { function resolveFromPublicAssets (id: string) {
@ -40,14 +41,33 @@ export const VitePublicDirsPlugin = createUnplugin(() => {
} }
}, },
}, },
generateBundle (outputOptions, bundle) { renderChunk (code, chunk) {
if (!chunk.facadeModuleId?.includes('?inline&used')) { return }
const s = new MagicString(code)
const q = code.match(/(?<= = )['"`]/)?.[0] || '"'
for (const [full, url] of code.matchAll(CSS_URL_RE)) {
if (resolveFromPublicAssets(url)) {
s.replace(full, `url(${q} + publicAssetsURL(${q}${url}${q}) + ${q})`)
}
}
if (s.hasChanged()) {
s.prepend(`import { publicAssetsURL } from '#internal/nuxt/paths';`)
return {
code: s.toString(),
map: options.sourcemap ? s.generateMap({ hires: true }) : undefined,
}
}
},
generateBundle (_outputOptions, bundle) {
for (const file in bundle) { for (const file in bundle) {
const chunk = bundle[file] const chunk = bundle[file]
if (!file.endsWith('.css') || chunk.type !== 'asset') { continue } if (!file.endsWith('.css') || chunk.type !== 'asset') { continue }
let css = chunk.source.toString() let css = chunk.source.toString()
let wasReplaced = false let wasReplaced = false
for (const [full, url] of css.matchAll(/url\((\/[^)]+)\)/g)) { for (const [full, url] of css.matchAll(CSS_URL_RE)) {
if (resolveFromPublicAssets(url)) { if (resolveFromPublicAssets(url)) {
const relativeURL = relative(withLeadingSlash(dirname(file)), url) const relativeURL = relative(withLeadingSlash(dirname(file)), url)
css = css.replace(full, `url(${relativeURL})`) css = css.replace(full, `url(${relativeURL})`)
@ -62,3 +82,5 @@ export const VitePublicDirsPlugin = createUnplugin(() => {
}, },
} }
}) })
const CSS_URL_RE = /url\((\/[^)]+)\)/g

View File

@ -100,7 +100,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
}, },
plugins: [ plugins: [
// add resolver for files in public assets directories // add resolver for files in public assets directories
VitePublicDirsPlugin.vite(), VitePublicDirsPlugin.vite({ sourcemap: !!nuxt.options.sourcemap.server }),
composableKeysPlugin.vite({ composableKeysPlugin.vite({
sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client, sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
rootDir: nuxt.options.rootDir, rootDir: nuxt.options.rootDir,

View File

@ -1915,11 +1915,24 @@ describe('public directories', () => {
// TODO: dynamic paths in dev // TODO: dynamic paths in dev
describe.skipIf(isDev())('dynamic paths', () => { describe.skipIf(isDev())('dynamic paths', () => {
const publicFiles = ['/public.svg', '/css-only-public-asset.svg']
const isPublicFile = (base = '/', file: string) => {
if (isWebpack) {
// TODO: webpack does not yet support dynamic static paths
expect(publicFiles).toContain(file)
return true
}
expect(file).toMatch(new RegExp(`^${base.replace(/\//g, '\\/')}`))
expect(publicFiles).toContain(file.replace(base, '/'))
return true
}
it('should work with no overrides', async () => { it('should work with no overrides', async () => {
const html: string = await $fetch<string>('/assets') const html: string = await $fetch<string>('/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) { for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3] const url = match[2] || match[3]
expect(url.startsWith('/_nuxt/') || url === '/public.svg').toBeTruthy() expect(url.startsWith('/_nuxt/') || isPublicFile('/', url)).toBeTruthy()
} }
}) })
@ -1929,14 +1942,12 @@ describe.skipIf(isDev())('dynamic paths', () => {
const urls = Array.from(html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)).map(m => m[2] || m[3]) const urls = Array.from(html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)).map(m => m[2] || m[3])
const cssURL = urls.find(u => /_nuxt\/assets.*\.css$/.test(u)) const cssURL = urls.find(u => /_nuxt\/assets.*\.css$/.test(u))
expect(cssURL).toBeDefined() expect(cssURL).toBeDefined()
const css: string = await $fetch<string>(cssURL!) const css = await $fetch<string>(cssURL!)
const imageUrls = Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1].replace(/[-.]\w{8}\./g, '.')) const imageUrls = new Set(Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1].replace(/[-.]\w{8}\./g, '.')))
expect(imageUrls).toMatchInlineSnapshot(` expect([...imageUrls]).toMatchInlineSnapshot(`
[ [
"./logo.svg", "./logo.svg",
"../public.svg", "../public.svg",
"../public.svg",
"../public.svg",
] ]
`) `)
}) })
@ -1952,12 +1963,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
const html = await $fetch<string>('/foo/assets') const html = await $fetch<string>('/foo/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) { for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3] const url = match[2] || match[3]
expect( expect(url.startsWith('/foo/_other/') || isPublicFile('/foo/', url)).toBeTruthy()
url.startsWith('/foo/_other/') ||
url === '/foo/public.svg' ||
// TODO: webpack does not yet support dynamic static paths
(isWebpack && url === '/public.svg'),
).toBeTruthy()
} }
expect(await $fetch<string>('/foo/url')).toContain('path: /foo/url') expect(await $fetch<string>('/foo/url')).toContain('path: /foo/url')
@ -1973,12 +1979,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
const html = await $fetch<string>('/assets') const html = await $fetch<string>('/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) { for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3] const url = match[2] || match[3]
expect( expect(url.startsWith('./_nuxt/') || isPublicFile('./', url)).toBeTruthy()
url.startsWith('./_nuxt/') ||
url === './public.svg' ||
// TODO: webpack does not yet support dynamic static paths
(isWebpack && url === '/public.svg'),
).toBeTruthy()
expect(url.startsWith('./_nuxt/_nuxt')).toBeFalsy() expect(url.startsWith('./_nuxt/_nuxt')).toBeFalsy()
} }
}) })
@ -2007,12 +2008,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
const html = await $fetch<string>('/foo/assets') const html = await $fetch<string>('/foo/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) { for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3] const url = match[2] || match[3]
expect( expect(url.startsWith('https://example.com/_cdn/') || isPublicFile('https://example.com/', url)).toBeTruthy()
url.startsWith('https://example.com/_cdn/') ||
url === 'https://example.com/public.svg' ||
// TODO: webpack does not yet support dynamic static paths
(isWebpack && url === '/public.svg'),
).toBeTruthy()
} }
}) })

View File

@ -1,4 +1,5 @@
:root { :root {
--global: 'global'; --global: 'global';
--asset: url('~/assets/css-only-asset.svg'); --asset: url('~/assets/css-only-asset.svg');
--public-asset: url('/css-only-public-asset.svg');
} }

View File

@ -0,0 +1 @@
<svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg"></svg>

After

Width:  |  Height:  |  Size: 67 B