diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index d17fc0845326dc..8148b34110022b 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -15,7 +15,7 @@ import { import { openBrowser } from './server/openBrowser' import compression from './server/middlewares/compression' import { proxyMiddleware } from './server/middlewares/proxy' -import { resolveHostname, resolveServerUrls } from './utils' +import { resolveHostname, resolveServerUrls, shouldServe } from './utils' import { printServerUrls } from './logger' import { resolveConfig } from '.' import type { InlineConfig, ResolvedConfig } from '.' @@ -112,21 +112,24 @@ export async function preview( // static assets const distDir = path.resolve(config.root, config.build.outDir) const headers = config.preview.headers - app.use( - previewBase, - sirv(distDir, { - etag: true, - dev: true, - single: config.appType === 'spa', - setHeaders(res) { - if (headers) { - for (const name in headers) { - res.setHeader(name, headers[name]!) - } + const assetServer = sirv(distDir, { + etag: true, + dev: true, + single: config.appType === 'spa', + setHeaders(res) { + if (headers) { + for (const name in headers) { + res.setHeader(name, headers[name]!) } } - }) - ) + } + }) + app.use(previewBase, async (req, res, next) => { + if (shouldServe(req.url!, distDir)) { + return assetServer(req, res, next) + } + next() + }) // apply post server hooks from plugins postHooks.forEach((fn) => fn && fn()) diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts index c877022a63cde7..8f6b47f3a7e4bb 100644 --- a/packages/vite/src/node/server/middlewares/static.ts +++ b/packages/vite/src/node/server/middlewares/static.ts @@ -14,6 +14,7 @@ import { isInternalRequest, isParentDirectory, isWindows, + shouldServe, slash } from '../../utils' @@ -52,7 +53,10 @@ export function servePublicMiddleware( if (isImportRequest(req.url!) || isInternalRequest(req.url!)) { return next() } - serve(req, res, next) + if (shouldServe(req.url!, dir)) { + return serve(req, res, next) + } + next() } } diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 23c7597bb77195..3c1e9c90f2a034 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -1198,6 +1198,45 @@ export const isNonDriveRelativeAbsolutePath = (p: string): boolean => { return windowsDrivePathPrefixRE.test(p) } +/** + * Determine if a file is being requested with the correct case, to ensure + * consistent behaviour between dev and prod and across operating systems. + */ +export function shouldServe(url: string, assetsDir: string): boolean { + // viteTestUrl is set to something like http://localhost:4173/ and then many tests make calls + // like `await page.goto(viteTestUrl + '/example')` giving us URLs beginning with a double slash + const pathname = decodeURI( + new URL(url.startsWith('//') ? url.substring(1) : url, 'http://example.com') + .pathname + ) + const file = path.join(assetsDir, pathname) + if ( + !fs.existsSync(file) || + (isCaseInsensitiveFS && // can skip case check on Linux + !fs.statSync(file).isDirectory() && + !hasCorrectCase(file, assetsDir)) + ) { + return false + } + return true +} + +/** + * Note that we can't use realpath here, because we don't want to follow + * symlinks. + */ +function hasCorrectCase(file: string, assets: string): boolean { + if (file === assets) return true + + const parent = path.dirname(file) + + if (fs.readdirSync(parent).includes(path.basename(file))) { + return hasCorrectCase(parent, assets) + } + + return false +} + export function joinUrlSegments(a: string, b: string): string { if (!a || !b) { return a || b || '' diff --git a/playground/assets/__tests__/assets.spec.ts b/playground/assets/__tests__/assets.spec.ts index 768fadf7f35ce5..f8246a4f3418b3 100644 --- a/playground/assets/__tests__/assets.spec.ts +++ b/playground/assets/__tests__/assets.spec.ts @@ -1,3 +1,4 @@ +import fetch from 'node-fetch' import { describe, expect, test } from 'vitest' import { browserLogs, @@ -12,6 +13,7 @@ import { readFile, readManifest, untilUpdated, + viteTestUrl, watcher } from '~utils' @@ -27,6 +29,11 @@ test('should have no 404s', () => { }) }) +test('should get a 404 when using incorrect case', async () => { + expect((await fetch(viteTestUrl + 'icon.png')).status).toBe(200) + expect((await fetch(viteTestUrl + 'ICON.png')).status).toBe(404) +}) + describe('injected scripts', () => { test('@vite/client', async () => { const hasClient = await page.$(