Skip to content

Commit 46d1352

Browse files
authoredMar 22, 2023
fix(html): public asset urls always being treated as paths (fix #11857) (#11870)
1 parent 9cce026 commit 46d1352

File tree

6 files changed

+280
-5
lines changed

6 files changed

+280
-5
lines changed
 

‎packages/vite/src/node/plugins/html.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
getHash,
1919
isDataUrl,
2020
isExternalUrl,
21+
isUrl,
2122
normalizePath,
2223
processSrcSet,
2324
removeLeadingSlash,
@@ -812,11 +813,13 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
812813
})
813814

814815
result = result.replace(publicAssetUrlRE, (_, fileHash) => {
815-
return normalizePath(
816-
toOutputPublicAssetFilePath(
817-
getPublicAssetFilename(fileHash, config)!,
818-
),
816+
const publicAssetPath = toOutputPublicAssetFilePath(
817+
getPublicAssetFilename(fileHash, config)!,
819818
)
819+
820+
return isUrl(publicAssetPath)
821+
? publicAssetPath
822+
: normalizePath(publicAssetPath)
820823
})
821824

822825
if (chunk && canInlineEntry) {

‎packages/vite/src/node/utils.ts

+9
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,15 @@ function testCaseInsensitiveFS() {
197197
return fs.existsSync(CLIENT_ENTRY.replace('client.mjs', 'cLiEnT.mjs'))
198198
}
199199

200+
export function isUrl(path: string): boolean {
201+
try {
202+
new URL(path)
203+
return true
204+
} catch {
205+
return false
206+
}
207+
}
208+
200209
export const isCaseInsensitiveFS = testCaseInsensitiveFS()
201210

202211
export const isWindows = os.platform() === 'win32'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import { beforeAll, describe, expect, test } from 'vitest'
2+
import {
3+
browserLogs,
4+
findAssetFile,
5+
getBg,
6+
getColor,
7+
isBuild,
8+
page,
9+
viteConfig,
10+
} from '~utils'
11+
12+
const urlAssetMatch = isBuild
13+
? /http:\/\/localhost:4173\/other-assets\/asset-\w{8}\.png/
14+
: '/nested/asset.png'
15+
16+
const iconMatch = '/icon.png'
17+
18+
const absoluteIconMatch = isBuild
19+
? /http:\/\/localhost:4173\/.*\/icon-\w{8}\.png/
20+
: '/nested/icon.png'
21+
22+
const absolutePublicIconMatch = isBuild
23+
? /http:\/\/localhost:4173\/icon\.png/
24+
: '/icon.png'
25+
26+
test('should have no 404s', () => {
27+
browserLogs.forEach((msg) => {
28+
expect(msg).not.toMatch('404')
29+
})
30+
})
31+
32+
describe('raw references from /public', () => {
33+
test('load raw js from /public', async () => {
34+
expect(await page.textContent('.raw-js')).toMatch('[success]')
35+
})
36+
37+
test('load raw css from /public', async () => {
38+
expect(await getColor('.raw-css')).toBe('red')
39+
})
40+
})
41+
42+
test('import-expression from simple script', async () => {
43+
expect(await page.textContent('.import-expression')).toMatch(
44+
'[success][success]',
45+
)
46+
})
47+
48+
describe('asset imports from js', () => {
49+
test('relative', async () => {
50+
expect(await page.textContent('.asset-import-relative')).toMatch(
51+
urlAssetMatch,
52+
)
53+
})
54+
55+
test('absolute', async () => {
56+
expect(await page.textContent('.asset-import-absolute')).toMatch(
57+
urlAssetMatch,
58+
)
59+
})
60+
61+
test('from /public', async () => {
62+
expect(await page.textContent('.public-import')).toMatch(
63+
absolutePublicIconMatch,
64+
)
65+
})
66+
})
67+
68+
describe('css url() references', () => {
69+
test('fonts', async () => {
70+
expect(
71+
await page.evaluate(() => document.fonts.check('700 32px Inter')),
72+
).toBe(true)
73+
})
74+
75+
test('relative', async () => {
76+
const bg = await getBg('.css-url-relative')
77+
expect(bg).toMatch(urlAssetMatch)
78+
})
79+
80+
test('image-set relative', async () => {
81+
const imageSet = await getBg('.css-image-set-relative')
82+
imageSet.split(', ').forEach((s) => {
83+
expect(s).toMatch(urlAssetMatch)
84+
})
85+
})
86+
87+
test('image-set without the url() call', async () => {
88+
const imageSet = await getBg('.css-image-set-without-url-call')
89+
imageSet.split(', ').forEach((s) => {
90+
expect(s).toMatch(urlAssetMatch)
91+
})
92+
})
93+
94+
test('image-set with var', async () => {
95+
const imageSet = await getBg('.css-image-set-with-var')
96+
imageSet.split(', ').forEach((s) => {
97+
expect(s).toMatch(urlAssetMatch)
98+
})
99+
})
100+
101+
test('image-set with mix', async () => {
102+
const imageSet = await getBg('.css-image-set-mix-url-var')
103+
imageSet.split(', ').forEach((s) => {
104+
expect(s).toMatch(urlAssetMatch)
105+
})
106+
})
107+
108+
test('relative in @import', async () => {
109+
expect(await getBg('.css-url-relative-at-imported')).toMatch(urlAssetMatch)
110+
})
111+
112+
test('absolute', async () => {
113+
expect(await getBg('.css-url-absolute')).toMatch(urlAssetMatch)
114+
})
115+
116+
test('from /public', async () => {
117+
expect(await getBg('.css-url-public')).toMatch(iconMatch)
118+
})
119+
120+
test('multiple urls on the same line', async () => {
121+
const bg = await getBg('.css-url-same-line')
122+
expect(bg).toMatch(urlAssetMatch)
123+
expect(bg).toMatch(iconMatch)
124+
})
125+
126+
test('aliased', async () => {
127+
const bg = await getBg('.css-url-aliased')
128+
expect(bg).toMatch(urlAssetMatch)
129+
})
130+
})
131+
132+
describe.runIf(isBuild)('index.css URLs', () => {
133+
let css: string
134+
beforeAll(() => {
135+
const base = viteConfig ? viteConfig?.testConfig?.baseRoute : ''
136+
css = findAssetFile(/index.*\.css$/, base, 'other-assets')
137+
})
138+
139+
test('use base URL for asset URL', () => {
140+
expect(css).toMatch(urlAssetMatch)
141+
})
142+
143+
test('preserve postfix query/hash', () => {
144+
expect(css).toMatch('woff2?#iefix')
145+
})
146+
})
147+
148+
describe('image', () => {
149+
test('srcset', async () => {
150+
const img = await page.$('.img-src-set')
151+
const srcset = await img.getAttribute('srcset')
152+
srcset.split(', ').forEach((s) => {
153+
expect(s).toMatch(
154+
isBuild
155+
? /other-assets\/asset-\w{8}\.png \dx/
156+
: /\.\/nested\/asset\.png \dx/,
157+
)
158+
})
159+
})
160+
})
161+
162+
describe('svg fragments', () => {
163+
// 404 is checked already, so here we just ensure the urls end with #fragment
164+
test('img url', async () => {
165+
const img = await page.$('.svg-frag-img')
166+
expect(await img.getAttribute('src')).toMatch(/svg#icon-clock-view$/)
167+
})
168+
169+
test('via css url()', async () => {
170+
const bg = await page.evaluate(
171+
() => getComputedStyle(document.querySelector('.icon')).backgroundImage,
172+
)
173+
expect(bg).toMatch(/svg#icon-clock-view"\)$/)
174+
})
175+
176+
test('from js import', async () => {
177+
const img = await page.$('.svg-frag-import')
178+
expect(await img.getAttribute('src')).toMatch(/svg#icon-heart-view$/)
179+
})
180+
})
181+
182+
test('?raw import', async () => {
183+
expect(await page.textContent('.raw')).toMatch('SVG')
184+
})
185+
186+
test('?url import', async () => {
187+
expect(await page.textContent('.url')).toMatch(
188+
isBuild
189+
? /http:\/\/localhost:4173\/other-assets\/foo-\w{8}\.js/
190+
: '/foo.js',
191+
)
192+
})
193+
194+
test('?url import on css', async () => {
195+
const txt = await page.textContent('.url-css')
196+
expect(txt).toMatch(
197+
isBuild
198+
? /http:\/\/localhost:4173\/other-assets\/icons-\w{8}\.css/
199+
: '/css/icons.css',
200+
)
201+
})
202+
203+
test('new URL(..., import.meta.url)', async () => {
204+
expect(await page.textContent('.import-meta-url')).toMatch(urlAssetMatch)
205+
})
206+
207+
test('new URL(`${dynamic}`, import.meta.url)', async () => {
208+
const dynamic1 = await page.textContent('.dynamic-import-meta-url-1')
209+
expect(dynamic1).toMatch(absoluteIconMatch)
210+
const dynamic2 = await page.textContent('.dynamic-import-meta-url-2')
211+
expect(dynamic2).toMatch(urlAssetMatch)
212+
})
213+
214+
test('new URL(`non-existent`, import.meta.url)', async () => {
215+
expect(await page.textContent('.non-existent-import-meta-url')).toMatch(
216+
'/non-existent',
217+
)
218+
})
219+
220+
test('inline style test', async () => {
221+
expect(await getBg('.inline-style')).toMatch(urlAssetMatch)
222+
expect(await getBg('.style-url-assets')).toMatch(urlAssetMatch)
223+
})
224+
225+
test('html import word boundary', async () => {
226+
expect(await page.textContent('.obj-import-express')).toMatch(
227+
'ignore object import prop',
228+
)
229+
expect(await page.textContent('.string-import-express')).toMatch('no load')
230+
})
231+
232+
test('relative path in html asset', async () => {
233+
expect(await page.textContent('.relative-js')).toMatch('hello')
234+
expect(await getColor('.relative-css')).toMatch('red')
235+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '../../vite.config-url-base'

‎playground/assets/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
"preview:relative-base": "vite --config ./vite.config-relative-base.js preview",
1313
"dev:runtime-base": "vite --config ./vite.config-runtime-base.js dev",
1414
"build:runtime-base": "vite --config ./vite.config-runtime-base.js build",
15-
"preview:runtime-base": "vite --config ./vite.config-runtime-base.js preview"
15+
"preview:runtime-base": "vite --config ./vite.config-runtime-base.js preview",
16+
"dev:url-base": "vite --config ./vite.config-url-base.js dev",
17+
"build:url-base": "vite --config ./vite.config-url-base.js build",
18+
"preview:url-base": "vite --config ./vite.config-url-base.js preview"
1619
}
1720
}
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { defineConfig } from 'vite'
2+
import baseConfig from './vite.config.js'
3+
4+
export default defineConfig({
5+
...baseConfig,
6+
base: 'http://localhost:4173/',
7+
build: {
8+
...baseConfig.build,
9+
outDir: 'dist/url-base',
10+
watch: null,
11+
minify: false,
12+
assetsInlineLimit: 0,
13+
rollupOptions: {
14+
output: {
15+
entryFileNames: 'entries/[name].js',
16+
chunkFileNames: 'chunks/[name]-[hash].js',
17+
assetFileNames: 'other-assets/[name]-[hash][extname]',
18+
},
19+
},
20+
},
21+
testConfig: {
22+
baseRoute: '/url-base/',
23+
},
24+
})

0 commit comments

Comments
 (0)
Please sign in to comment.