mirror of
https://github.com/vitejs/vite.git
synced 2024-11-21 14:48:41 +00:00
fix: handle encoded base paths (#17577)
This commit is contained in:
parent
1025bb6d8f
commit
720447ee72
@ -83,7 +83,7 @@ function toOutputFilePathInHtml(
|
||||
if (relative && !config.build.ssr) {
|
||||
return toRelative(filename, hostId)
|
||||
} else {
|
||||
return config.base + filename
|
||||
return joinUrlSegments(config.decodedBase, filename)
|
||||
}
|
||||
}
|
||||
function getBaseInHTML(urlRelativePath: string, config: ResolvedConfig) {
|
||||
@ -96,6 +96,18 @@ function getBaseInHTML(urlRelativePath: string, config: ResolvedConfig) {
|
||||
)
|
||||
: config.base
|
||||
}
|
||||
function joinUrlSegments(a: string, b: string): string {
|
||||
if (!a || !b) {
|
||||
return a || b || ''
|
||||
}
|
||||
if (a[a.length - 1] === '/') {
|
||||
a = a.substring(0, a.length - 1)
|
||||
}
|
||||
if (b[0] !== '/') {
|
||||
b = '/' + b
|
||||
}
|
||||
return a + b
|
||||
}
|
||||
|
||||
function toAssetPathFromHtml(
|
||||
filename: string,
|
||||
|
@ -1226,7 +1226,7 @@ export function toOutputFilePathInJS(
|
||||
if (relative && !config.build.ssr) {
|
||||
return toRelative(filename, hostId)
|
||||
}
|
||||
return joinUrlSegments(config.base, filename)
|
||||
return joinUrlSegments(config.decodedBase, filename)
|
||||
}
|
||||
|
||||
export function createToImportMetaURLBasedRelativeRuntime(
|
||||
@ -1275,7 +1275,7 @@ export function toOutputFilePathWithoutRuntime(
|
||||
if (relative && !config.build.ssr) {
|
||||
return toRelative(filename, hostId)
|
||||
} else {
|
||||
return joinUrlSegments(config.base, filename)
|
||||
return joinUrlSegments(config.decodedBase, filename)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -365,6 +365,8 @@ export type ResolvedConfig = Readonly<
|
||||
root: string
|
||||
base: string
|
||||
/** @internal */
|
||||
decodedBase: string
|
||||
/** @internal */
|
||||
rawBase: string
|
||||
publicDir: string
|
||||
cacheDir: string
|
||||
@ -763,6 +765,8 @@ export async function resolveConfig(
|
||||
rollupOptions: config.worker?.rollupOptions || {},
|
||||
}
|
||||
|
||||
const base = withTrailingSlash(resolvedBase)
|
||||
|
||||
resolved = {
|
||||
configFile: configFile ? normalizePath(configFile) : undefined,
|
||||
configFileDependencies: configFileDependencies.map((name) =>
|
||||
@ -770,7 +774,8 @@ export async function resolveConfig(
|
||||
),
|
||||
inlineConfig,
|
||||
root: resolvedRoot,
|
||||
base: withTrailingSlash(resolvedBase),
|
||||
base,
|
||||
decodedBase: decodeURI(base),
|
||||
rawBase: resolvedBase,
|
||||
resolve: resolveOptions,
|
||||
publicDir: resolvedPublicDir,
|
||||
|
@ -281,7 +281,7 @@ function fileToDevUrl(id: string, config: ResolvedConfig) {
|
||||
// (this is special handled by the serve static middleware
|
||||
rtn = path.posix.join(FS_PREFIX, id)
|
||||
}
|
||||
const base = joinUrlSegments(config.server?.origin ?? '', config.base)
|
||||
const base = joinUrlSegments(config.server?.origin ?? '', config.decodedBase)
|
||||
return joinUrlSegments(base, removeLeadingSlash(rtn))
|
||||
}
|
||||
|
||||
@ -306,7 +306,7 @@ export function publicFileToBuiltUrl(
|
||||
): string {
|
||||
if (config.command !== 'build') {
|
||||
// We don't need relative base or renderBuiltUrl support during dev
|
||||
return joinUrlSegments(config.base, url)
|
||||
return joinUrlSegments(config.decodedBase, url)
|
||||
}
|
||||
const hash = getHash(url)
|
||||
let cache = publicAssetUrlCache.get(config)
|
||||
|
@ -167,7 +167,13 @@ const processNodeUrl = (
|
||||
)
|
||||
}
|
||||
if (preTransformUrl) {
|
||||
preTransformRequest(server, preTransformUrl, config.base)
|
||||
try {
|
||||
preTransformUrl = decodeURI(preTransformUrl)
|
||||
} catch (err) {
|
||||
// Malformed uri. Skip pre-transform.
|
||||
return url
|
||||
}
|
||||
preTransformRequest(server, preTransformUrl, config.decodedBase)
|
||||
}
|
||||
}
|
||||
return url
|
||||
@ -184,6 +190,7 @@ const devHtmlHook: IndexHtmlTransformHook = async (
|
||||
) => {
|
||||
const { config, moduleGraph, watcher } = server!
|
||||
const base = config.base || '/'
|
||||
const decodedBase = config.decodedBase || '/'
|
||||
|
||||
let proxyModulePath: string
|
||||
let proxyModuleUrl: string
|
||||
@ -202,7 +209,7 @@ const devHtmlHook: IndexHtmlTransformHook = async (
|
||||
proxyModulePath = `\0${validPath}`
|
||||
proxyModuleUrl = wrapId(proxyModulePath)
|
||||
}
|
||||
proxyModuleUrl = joinUrlSegments(base, proxyModuleUrl)
|
||||
proxyModuleUrl = joinUrlSegments(decodedBase, proxyModuleUrl)
|
||||
|
||||
const s = new MagicString(html)
|
||||
let inlineModuleIndex = -1
|
||||
@ -252,7 +259,7 @@ const devHtmlHook: IndexHtmlTransformHook = async (
|
||||
node.sourceCodeLocation!.endOffset,
|
||||
`<script type="module" src="${modulePath}"></script>`,
|
||||
)
|
||||
preTransformRequest(server!, modulePath, base)
|
||||
preTransformRequest(server!, modulePath, decodedBase)
|
||||
}
|
||||
|
||||
await traverseHtml(html, filename, (node) => {
|
||||
@ -447,15 +454,16 @@ export function indexHtmlMiddleware(
|
||||
}
|
||||
}
|
||||
|
||||
function preTransformRequest(server: ViteDevServer, url: string, base: string) {
|
||||
// NOTE: We usually don't prefix `url` and `base` with `decoded`, but in this file particularly
|
||||
// we're dealing with mixed encoded/decoded paths often, so we make this explicit for now.
|
||||
function preTransformRequest(
|
||||
server: ViteDevServer,
|
||||
decodedUrl: string,
|
||||
decodedBase: string,
|
||||
) {
|
||||
if (!server.config.server.preTransformRequests) return
|
||||
|
||||
// transform all url as non-ssr as html includes client-side assets only
|
||||
try {
|
||||
url = unwrapId(stripBase(decodeURI(url), base))
|
||||
} catch {
|
||||
// ignore
|
||||
return
|
||||
}
|
||||
server.warmupRequest(url)
|
||||
decodedUrl = unwrapId(stripBase(decodedUrl, decodedBase))
|
||||
server.warmupRequest(decodedUrl)
|
||||
}
|
||||
|
@ -0,0 +1,229 @@
|
||||
import { beforeAll, describe, expect, test } from 'vitest'
|
||||
import {
|
||||
browserLogs,
|
||||
findAssetFile,
|
||||
getBg,
|
||||
getColor,
|
||||
isBuild,
|
||||
page,
|
||||
} from '~utils'
|
||||
|
||||
const urlAssetMatch = isBuild
|
||||
? /\/foo%20bar\/other-assets\/asset-[-\w]{8}\.png/
|
||||
: '/nested/asset.png'
|
||||
|
||||
const iconMatch = '/icon.png'
|
||||
|
||||
const absoluteIconMatch = isBuild
|
||||
? /\/foo%20bar\/.*\/icon-[-\w]{8}\.png/
|
||||
: '/nested/icon.png'
|
||||
|
||||
const absolutePublicIconMatch = isBuild ? /\/foo%20bar\/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(() => {
|
||||
css = findAssetFile(/index.*\.css$/, 'encoded-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
|
||||
? /\/foo%20bar\/other-assets\/asset-[-\w]{8}\.png \dx/
|
||||
: /\/foo%20bar\/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 ? /\/foo%20bar\/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
|
||||
? /\/foo%20bar\/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')
|
||||
})
|
@ -8,6 +8,9 @@
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"dev:encoded-base": "vite --config ./vite.config-encoded-base.js dev",
|
||||
"build:encoded-base": "vite --config ./vite.config-encoded-base.js build",
|
||||
"preview:encoded-base": "vite --config ./vite.config-encoded-base.js preview",
|
||||
"dev:relative-base": "vite --config ./vite.config-relative-base.js dev",
|
||||
"build:relative-base": "vite --config ./vite.config-relative-base.js build",
|
||||
"preview:relative-base": "vite --config ./vite.config-relative-base.js preview",
|
||||
|
34
playground/assets/vite.config-encoded-base.js
Normal file
34
playground/assets/vite.config-encoded-base.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import baseConfig from './vite.config.js'
|
||||
|
||||
/** see `ports` variable in test-utils.ts */
|
||||
const port = 9524
|
||||
|
||||
export default defineConfig({
|
||||
...baseConfig,
|
||||
// Vite should auto-encode this as `/foo%20bar/` internally
|
||||
base: '/foo bar/',
|
||||
server: {
|
||||
port,
|
||||
strictPort: true,
|
||||
},
|
||||
build: {
|
||||
...baseConfig.build,
|
||||
outDir: 'dist/encoded-base',
|
||||
watch: null,
|
||||
minify: false,
|
||||
assetsInlineLimit: 0,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: 'entries/[name].js',
|
||||
chunkFileNames: 'chunks/[name]-[hash].js',
|
||||
assetFileNames: 'other-assets/[name]-[hash][extname]',
|
||||
},
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
port,
|
||||
strictPort: true,
|
||||
},
|
||||
cacheDir: 'node_modules/.vite-encoded-base',
|
||||
})
|
@ -2,7 +2,7 @@ import { defineConfig } from 'vite'
|
||||
import baseConfig from './vite.config.js'
|
||||
|
||||
/** see `ports` variable in test-utils.ts */
|
||||
const port = 9524
|
||||
const port = 9525
|
||||
|
||||
export default defineConfig({
|
||||
...baseConfig,
|
||||
|
@ -28,7 +28,8 @@ export const ports = {
|
||||
lib: 9521,
|
||||
'optimize-missing-deps': 9522,
|
||||
'legacy/client-and-ssr': 9523,
|
||||
'assets/url-base': 9524, // not imported but used in `assets/vite.config-url-base.js`
|
||||
'assets/encoded-base': 9554, // not imported but used in `assets/vite.config-encoded-base.js`
|
||||
'assets/url-base': 9525, // not imported but used in `assets/vite.config-url-base.js`
|
||||
ssr: 9600,
|
||||
'ssr-deps': 9601,
|
||||
'ssr-html': 9602,
|
||||
|
Loading…
Reference in New Issue
Block a user