From c054ca084c14ca8a11dd8f60c2a15cce291faefa Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 23 May 2024 15:34:06 +0100 Subject: [PATCH] fix(vite): handle runtime paths in inlined styles (#27327) --- packages/vite/src/plugins/public-dirs.ts | 28 ++++++++-- packages/vite/src/vite.ts | 2 +- test/basic.test.ts | 54 +++++++++---------- test/fixtures/basic/assets/global.css | 1 + .../basic/public/css-only-public-asset.svg | 1 + 5 files changed, 53 insertions(+), 33 deletions(-) create mode 100644 test/fixtures/basic/public/css-only-public-asset.svg diff --git a/packages/vite/src/plugins/public-dirs.ts b/packages/vite/src/plugins/public-dirs.ts index 4a29309079..29e4a305ea 100644 --- a/packages/vite/src/plugins/public-dirs.ts +++ b/packages/vite/src/plugins/public-dirs.ts @@ -3,10 +3,11 @@ import { useNitro } from '@nuxt/kit' import { createUnplugin } from 'unplugin' import { withLeadingSlash, withTrailingSlash } from 'ufo' import { dirname, relative } from 'pathe' +import MagicString from 'magic-string' const PREFIX = 'virtual:public?' -export const VitePublicDirsPlugin = createUnplugin(() => { +export const VitePublicDirsPlugin = createUnplugin((options: { sourcemap?: boolean }) => { const nitro = useNitro() 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) { const chunk = bundle[file] if (!file.endsWith('.css') || chunk.type !== 'asset') { continue } let css = chunk.source.toString() 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)) { const relativeURL = relative(withLeadingSlash(dirname(file)), url) css = css.replace(full, `url(${relativeURL})`) @@ -62,3 +82,5 @@ export const VitePublicDirsPlugin = createUnplugin(() => { }, } }) + +const CSS_URL_RE = /url\((\/[^)]+)\)/g diff --git a/packages/vite/src/vite.ts b/packages/vite/src/vite.ts index a247c181cc..e0a97a0cd1 100644 --- a/packages/vite/src/vite.ts +++ b/packages/vite/src/vite.ts @@ -100,7 +100,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => { }, plugins: [ // add resolver for files in public assets directories - VitePublicDirsPlugin.vite(), + VitePublicDirsPlugin.vite({ sourcemap: !!nuxt.options.sourcemap.server }), composableKeysPlugin.vite({ sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client, rootDir: nuxt.options.rootDir, diff --git a/test/basic.test.ts b/test/basic.test.ts index 41bd0c5f11..da188dfc68 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -1915,11 +1915,24 @@ describe('public directories', () => { // TODO: dynamic paths in dev 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 () => { const html: string = await $fetch('/assets') for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) { const url = match[2] || match[3] - expect(url.startsWith('/_nuxt/') || url === '/public.svg').toBeTruthy() + expect(url.startsWith('/_nuxt/') || isPublicFile('/', url)).toBeTruthy() } }) @@ -1929,16 +1942,14 @@ describe.skipIf(isDev())('dynamic paths', () => { 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)) expect(cssURL).toBeDefined() - const css: string = await $fetch(cssURL!) - const imageUrls = Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1].replace(/[-.]\w{8}\./g, '.')) - expect(imageUrls).toMatchInlineSnapshot(` - [ - "./logo.svg", - "../public.svg", - "../public.svg", - "../public.svg", - ] - `) + const css = await $fetch(cssURL!) + const imageUrls = new Set(Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1].replace(/[-.]\w{8}\./g, '.'))) + expect([...imageUrls]).toMatchInlineSnapshot(` + [ + "./logo.svg", + "../public.svg", + ] + `) }) it('should allow setting base URL and build assets directory', async () => { @@ -1952,12 +1963,7 @@ describe.skipIf(isDev())('dynamic paths', () => { const html = await $fetch('/foo/assets') for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) { const url = match[2] || match[3] - expect( - url.startsWith('/foo/_other/') || - url === '/foo/public.svg' || - // TODO: webpack does not yet support dynamic static paths - (isWebpack && url === '/public.svg'), - ).toBeTruthy() + expect(url.startsWith('/foo/_other/') || isPublicFile('/foo/', url)).toBeTruthy() } expect(await $fetch('/foo/url')).toContain('path: /foo/url') @@ -1973,12 +1979,7 @@ describe.skipIf(isDev())('dynamic paths', () => { const html = await $fetch('/assets') for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) { const url = match[2] || match[3] - expect( - url.startsWith('./_nuxt/') || - url === './public.svg' || - // TODO: webpack does not yet support dynamic static paths - (isWebpack && url === '/public.svg'), - ).toBeTruthy() + expect(url.startsWith('./_nuxt/') || isPublicFile('./', url)).toBeTruthy() expect(url.startsWith('./_nuxt/_nuxt')).toBeFalsy() } }) @@ -2007,12 +2008,7 @@ describe.skipIf(isDev())('dynamic paths', () => { const html = await $fetch('/foo/assets') for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) { const url = match[2] || match[3] - expect( - 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() + expect(url.startsWith('https://example.com/_cdn/') || isPublicFile('https://example.com/', url)).toBeTruthy() } }) diff --git a/test/fixtures/basic/assets/global.css b/test/fixtures/basic/assets/global.css index 3d0b218a70..81bead5317 100644 --- a/test/fixtures/basic/assets/global.css +++ b/test/fixtures/basic/assets/global.css @@ -1,4 +1,5 @@ :root { --global: 'global'; --asset: url('~/assets/css-only-asset.svg'); + --public-asset: url('/css-only-public-asset.svg'); } diff --git a/test/fixtures/basic/public/css-only-public-asset.svg b/test/fixtures/basic/public/css-only-public-asset.svg new file mode 100644 index 0000000000..dce8bc0236 --- /dev/null +++ b/test/fixtures/basic/public/css-only-public-asset.svg @@ -0,0 +1 @@ +