fix(html): public asset urls always being treated as paths (fix #11857) (#11870)

This commit is contained in:
Alex Miller 2023-03-22 23:07:07 +13:00 committed by GitHub
parent 9cce02684c
commit 46d1352baf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 280 additions and 5 deletions

View File

@ -18,6 +18,7 @@ import {
getHash,
isDataUrl,
isExternalUrl,
isUrl,
normalizePath,
processSrcSet,
removeLeadingSlash,
@ -812,11 +813,13 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
})
result = result.replace(publicAssetUrlRE, (_, fileHash) => {
return normalizePath(
toOutputPublicAssetFilePath(
getPublicAssetFilename(fileHash, config)!,
),
const publicAssetPath = toOutputPublicAssetFilePath(
getPublicAssetFilename(fileHash, config)!,
)
return isUrl(publicAssetPath)
? publicAssetPath
: normalizePath(publicAssetPath)
})
if (chunk && canInlineEntry) {

View File

@ -197,6 +197,15 @@ function testCaseInsensitiveFS() {
return fs.existsSync(CLIENT_ENTRY.replace('client.mjs', 'cLiEnT.mjs'))
}
export function isUrl(path: string): boolean {
try {
new URL(path)
return true
} catch {
return false
}
}
export const isCaseInsensitiveFS = testCaseInsensitiveFS()
export const isWindows = os.platform() === 'win32'

View File

@ -0,0 +1,235 @@
import { beforeAll, describe, expect, test } from 'vitest'
import {
browserLogs,
findAssetFile,
getBg,
getColor,
isBuild,
page,
viteConfig,
} from '~utils'
const urlAssetMatch = isBuild
? /http:\/\/localhost:4173\/other-assets\/asset-\w{8}\.png/
: '/nested/asset.png'
const iconMatch = '/icon.png'
const absoluteIconMatch = isBuild
? /http:\/\/localhost:4173\/.*\/icon-\w{8}\.png/
: '/nested/icon.png'
const absolutePublicIconMatch = isBuild
? /http:\/\/localhost:4173\/icon\.png/
: '/icon.png'
test('should have no 404s', () => {
browserLogs.forEach((msg) => {
expect(msg).not.toMatch('404')
})
})
describe('raw references from /public', () => {
test('load raw js from /public', async () => {
expect(await page.textContent('.raw-js')).toMatch('[success]')
})
test('load raw css from /public', async () => {
expect(await getColor('.raw-css')).toBe('red')
})
})
test('import-expression from simple script', async () => {
expect(await page.textContent('.import-expression')).toMatch(
'[success][success]',
)
})
describe('asset imports from js', () => {
test('relative', async () => {
expect(await page.textContent('.asset-import-relative')).toMatch(
urlAssetMatch,
)
})
test('absolute', async () => {
expect(await page.textContent('.asset-import-absolute')).toMatch(
urlAssetMatch,
)
})
test('from /public', async () => {
expect(await page.textContent('.public-import')).toMatch(
absolutePublicIconMatch,
)
})
})
describe('css url() references', () => {
test('fonts', async () => {
expect(
await page.evaluate(() => document.fonts.check('700 32px Inter')),
).toBe(true)
})
test('relative', async () => {
const bg = await getBg('.css-url-relative')
expect(bg).toMatch(urlAssetMatch)
})
test('image-set relative', async () => {
const imageSet = await getBg('.css-image-set-relative')
imageSet.split(', ').forEach((s) => {
expect(s).toMatch(urlAssetMatch)
})
})
test('image-set without the url() call', async () => {
const imageSet = await getBg('.css-image-set-without-url-call')
imageSet.split(', ').forEach((s) => {
expect(s).toMatch(urlAssetMatch)
})
})
test('image-set with var', async () => {
const imageSet = await getBg('.css-image-set-with-var')
imageSet.split(', ').forEach((s) => {
expect(s).toMatch(urlAssetMatch)
})
})
test('image-set with mix', async () => {
const imageSet = await getBg('.css-image-set-mix-url-var')
imageSet.split(', ').forEach((s) => {
expect(s).toMatch(urlAssetMatch)
})
})
test('relative in @import', async () => {
expect(await getBg('.css-url-relative-at-imported')).toMatch(urlAssetMatch)
})
test('absolute', async () => {
expect(await getBg('.css-url-absolute')).toMatch(urlAssetMatch)
})
test('from /public', async () => {
expect(await getBg('.css-url-public')).toMatch(iconMatch)
})
test('multiple urls on the same line', async () => {
const bg = await getBg('.css-url-same-line')
expect(bg).toMatch(urlAssetMatch)
expect(bg).toMatch(iconMatch)
})
test('aliased', async () => {
const bg = await getBg('.css-url-aliased')
expect(bg).toMatch(urlAssetMatch)
})
})
describe.runIf(isBuild)('index.css URLs', () => {
let css: string
beforeAll(() => {
const base = viteConfig ? viteConfig?.testConfig?.baseRoute : ''
css = findAssetFile(/index.*\.css$/, base, 'other-assets')
})
test('use base URL for asset URL', () => {
expect(css).toMatch(urlAssetMatch)
})
test('preserve postfix query/hash', () => {
expect(css).toMatch('woff2?#iefix')
})
})
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
? /other-assets\/asset-\w{8}\.png \dx/
: /\.\/nested\/asset\.png \dx/,
)
})
})
})
describe('svg fragments', () => {
// 404 is checked already, so here we just ensure the urls end with #fragment
test('img url', async () => {
const img = await page.$('.svg-frag-img')
expect(await img.getAttribute('src')).toMatch(/svg#icon-clock-view$/)
})
test('via css url()', async () => {
const bg = await page.evaluate(
() => getComputedStyle(document.querySelector('.icon')).backgroundImage,
)
expect(bg).toMatch(/svg#icon-clock-view"\)$/)
})
test('from js import', async () => {
const img = await page.$('.svg-frag-import')
expect(await img.getAttribute('src')).toMatch(/svg#icon-heart-view$/)
})
})
test('?raw import', async () => {
expect(await page.textContent('.raw')).toMatch('SVG')
})
test('?url import', async () => {
expect(await page.textContent('.url')).toMatch(
isBuild
? /http:\/\/localhost:4173\/other-assets\/foo-\w{8}\.js/
: '/foo.js',
)
})
test('?url import on css', async () => {
const txt = await page.textContent('.url-css')
expect(txt).toMatch(
isBuild
? /http:\/\/localhost:4173\/other-assets\/icons-\w{8}\.css/
: '/css/icons.css',
)
})
test('new URL(..., import.meta.url)', async () => {
expect(await page.textContent('.import-meta-url')).toMatch(urlAssetMatch)
})
test('new URL(`${dynamic}`, import.meta.url)', async () => {
const dynamic1 = await page.textContent('.dynamic-import-meta-url-1')
expect(dynamic1).toMatch(absoluteIconMatch)
const dynamic2 = await page.textContent('.dynamic-import-meta-url-2')
expect(dynamic2).toMatch(urlAssetMatch)
})
test('new URL(`non-existent`, import.meta.url)', async () => {
expect(await page.textContent('.non-existent-import-meta-url')).toMatch(
'/non-existent',
)
})
test('inline style test', async () => {
expect(await getBg('.inline-style')).toMatch(urlAssetMatch)
expect(await getBg('.style-url-assets')).toMatch(urlAssetMatch)
})
test('html import word boundary', async () => {
expect(await page.textContent('.obj-import-express')).toMatch(
'ignore object import prop',
)
expect(await page.textContent('.string-import-express')).toMatch('no load')
})
test('relative path in html asset', async () => {
expect(await page.textContent('.relative-js')).toMatch('hello')
expect(await getColor('.relative-css')).toMatch('red')
})

View File

@ -0,0 +1 @@
export { default } from '../../vite.config-url-base'

View File

@ -12,6 +12,9 @@
"preview:relative-base": "vite --config ./vite.config-relative-base.js preview",
"dev:runtime-base": "vite --config ./vite.config-runtime-base.js dev",
"build:runtime-base": "vite --config ./vite.config-runtime-base.js build",
"preview:runtime-base": "vite --config ./vite.config-runtime-base.js preview"
"preview:runtime-base": "vite --config ./vite.config-runtime-base.js preview",
"dev:url-base": "vite --config ./vite.config-url-base.js dev",
"build:url-base": "vite --config ./vite.config-url-base.js build",
"preview:url-base": "vite --config ./vite.config-url-base.js preview"
}
}

View File

@ -0,0 +1,24 @@
import { defineConfig } from 'vite'
import baseConfig from './vite.config.js'
export default defineConfig({
...baseConfig,
base: 'http://localhost:4173/',
build: {
...baseConfig.build,
outDir: 'dist/url-base',
watch: null,
minify: false,
assetsInlineLimit: 0,
rollupOptions: {
output: {
entryFileNames: 'entries/[name].js',
chunkFileNames: 'chunks/[name]-[hash].js',
assetFileNames: 'other-assets/[name]-[hash][extname]',
},
},
},
testConfig: {
baseRoute: '/url-base/',
},
})