From e6846e3a8b5072c166e62011fbf025f8ff9a011e Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Fri, 14 Oct 2022 13:02:27 -0700 Subject: [PATCH] feat: handle static assets in case-sensitive manner --- packages/vite/src/node/preview.ts | 37 ++++++++++++------- .../src/node/server/middlewares/static.ts | 7 +++- packages/vite/src/node/utils.ts | 32 ++++++++++++++++ playground/assets/__tests__/assets.spec.ts | 7 ++++ 4 files changed, 68 insertions(+), 15 deletions(-) diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index d17fc0845326dc..7af1fff8cbb68e 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,30 @@ 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 asset_server = 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) => { + // TODO: why is this necessary? what's screwing up the request URL? + // tons of tests fail without this since we're receiving URLs like //assets/dep-42fa3c.js + const fixedUrl = req.url!.startsWith('//') + ? req.url!.substring(1) + : req.url! + const url = new URL(fixedUrl, 'http://example.com') + if (shouldServe(url, distDir)) { + return asset_server(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..07c45a4ee58bea 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,11 @@ export function servePublicMiddleware( if (isImportRequest(req.url!) || isInternalRequest(req.url!)) { return next() } - serve(req, res, next) + const url = new URL(req.url!, 'http://example.com') + if (shouldServe(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 2711a29b7eb703..074bb2007e7fd5 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -1190,3 +1190,35 @@ export const isNonDriveRelativeAbsolutePath = (p: string): boolean => { if (!isWindows) return p.startsWith('/') 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: URL, assetsDir: string): boolean { + const pathname = decodeURIComponent(url.pathname) + const file = assetsDir + pathname + if ( + !fs.existsSync(file) || + (!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 +} 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.$(