`).
+Note that the browser will no longer show a 404 error message in the console if you point the image path to a non-existent file (e.g. `
`).
+
+### Align dev and preview HTML serving behaviour
+
+In Vite 4, the dev and preview servers serve HTML based on its directory structure and trailing slash differently. This causes inconsistencies when testing your built app. Vite 5 refactors into a single behaviour like below, given the following file structure:
+
+```
+├── index.html
+├── file.html
+└── dir
+ └── index.html
+```
+
+| Request | Before (dev) | Before (preview) | After (dev & preview) |
+| ----------------- | ---------------------------- | ----------------- | ---------------------------- |
+| `/dir/index.html` | `/dir/index.html` | `/dir/index.html` | `/dir/index.html` |
+| `/dir` | `/index.html` (SPA fallback) | `/dir/index.html` | `/dir.html` (SPA fallback) |
+| `/dir/` | `/dir/index.html` | `/dir/index.html` | `/dir/index.html` |
+| `/file.html` | `/file.html` | `/file.html` | `/file.html` |
+| `/file` | `/index.html` (SPA fallback) | `/file.html` | `/file.html` |
+| `/file/` | `/index.html` (SPA fallback) | `/file.html` | `/index.html` (SPA fallback) |
### Manifest files are now generated in `.vite` directory by default
diff --git a/packages/vite/LICENSE.md b/packages/vite/LICENSE.md
index 03b0ef717114e4..7b45e464cf268f 100644
--- a/packages/vite/LICENSE.md
+++ b/packages/vite/LICENSE.md
@@ -911,35 +911,6 @@ Repository: senchalabs/connect
---------------------------------------
-## connect-history-api-fallback
-License: MIT
-By: Ben Ripkens, Craig Myles
-Repository: http://github.com/bripkens/connect-history-api-fallback.git
-
-> The MIT License
->
-> Copyright (c) 2022 Ben Blackmore and contributors
->
-> Permission is hereby granted, free of charge, to any person obtaining a copy
-> of this software and associated documentation files (the "Software"), to deal
-> in the Software without restriction, including without limitation the rights
-> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-> copies of the Software, and to permit persons to whom the Software is
-> furnished to do so, subject to the following conditions:
->
-> The above copyright notice and this permission notice shall be included in
-> all copies or substantial portions of the Software.
->
-> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-> THE SOFTWARE.
-
----------------------------------------
-
## convert-source-map
License: MIT
By: Thorsten Lorenz
diff --git a/packages/vite/package.json b/packages/vite/package.json
index 1606719790f67f..831a138585f27f 100644
--- a/packages/vite/package.json
+++ b/packages/vite/package.json
@@ -98,7 +98,6 @@
"cac": "^6.7.14",
"chokidar": "^3.5.3",
"connect": "^3.7.0",
- "connect-history-api-fallback": "^2.0.0",
"convert-source-map": "^2.0.0",
"cors": "^2.8.5",
"cross-spawn": "^7.0.3",
diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts
index e678bc6341d658..3d9329e581f3d8 100644
--- a/packages/vite/src/node/preview.ts
+++ b/packages/vite/src/node/preview.ts
@@ -15,6 +15,9 @@ import {
} from './http'
import { openBrowser } from './server/openBrowser'
import compression from './server/middlewares/compression'
+import { htmlFallbackMiddleware } from './server/middlewares/htmlFallback'
+import { indexHtmlMiddleware } from './server/middlewares/indexHtml'
+import { notFoundMiddleware } from './server/middlewares/notFound'
import { proxyMiddleware } from './server/middlewares/proxy'
import { resolveHostname, resolveServerUrls, shouldServeFile } from './utils'
import { printServerUrls } from './logger'
@@ -170,7 +173,7 @@ export async function preview(
sirv(distDir, {
etag: true,
dev: true,
- single: config.appType === 'spa',
+ extensions: [],
ignores: false,
setHeaders(res) {
if (headers) {
@@ -186,9 +189,29 @@ export async function preview(
app.use(previewBase, viteAssetMiddleware)
+ // html fallback
+ if (config.appType === 'spa' || config.appType === 'mpa') {
+ app.use(
+ previewBase,
+ htmlFallbackMiddleware(
+ distDir,
+ config.appType === 'spa',
+ previewBase !== '/',
+ ),
+ )
+ }
+
// apply post server hooks from plugins
postHooks.forEach((fn) => fn && fn())
+ if (config.appType === 'spa' || config.appType === 'mpa') {
+ // transform index.html
+ app.use(previewBase, indexHtmlMiddleware(distDir, server))
+
+ // handle 404s
+ app.use(previewBase, notFoundMiddleware())
+ }
+
const hostname = await resolveHostname(options.host)
const port = options.port ?? DEFAULT_PREVIEW_PORT
const protocol = options.https ? 'https' : 'http'
diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts
index e58dc4d1e3ea47..25b1e4efd0e7a4 100644
--- a/packages/vite/src/node/server/index.ts
+++ b/packages/vite/src/node/server/index.ts
@@ -66,6 +66,7 @@ import {
import { timeMiddleware } from './middlewares/time'
import type { ModuleNode } from './moduleGraph'
import { ModuleGraph } from './moduleGraph'
+import { notFoundMiddleware } from './middlewares/notFound'
import { errorMiddleware, prepareError } from './middlewares/error'
import type { HmrOptions } from './hmr'
import {
@@ -692,14 +693,10 @@ export async function _createServer(
if (config.appType === 'spa' || config.appType === 'mpa') {
// transform index.html
- middlewares.use(indexHtmlMiddleware(server))
+ middlewares.use(indexHtmlMiddleware(root, server))
// handle 404s
- // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
- middlewares.use(function vite404Middleware(_, res) {
- res.statusCode = 404
- res.end()
- })
+ middlewares.use(notFoundMiddleware())
}
// error handler
diff --git a/packages/vite/src/node/server/middlewares/htmlFallback.ts b/packages/vite/src/node/server/middlewares/htmlFallback.ts
index 39bf8ae2e32e17..85f5e396e54f8c 100644
--- a/packages/vite/src/node/server/middlewares/htmlFallback.ts
+++ b/packages/vite/src/node/server/middlewares/htmlFallback.ts
@@ -1,48 +1,86 @@
import fs from 'node:fs'
import path from 'node:path'
-import history from 'connect-history-api-fallback'
import type { Connect } from 'dep-types/connect'
-import { createDebugger } from '../../utils'
+import { cleanUrl, createDebugger } from '../../utils'
+
+const debug = createDebugger('vite:html-fallback')
export function htmlFallbackMiddleware(
root: string,
spaFallback: boolean,
+ mounted = false,
): Connect.NextHandleFunction {
- const historyHtmlFallbackMiddleware = history({
- disableDotRule: true,
- logger: createDebugger('vite:html-fallback'),
- rewrites: [
- // support /dir/ without explicit index.html
- {
- from: /\/$/,
- to({ parsedUrl, request }: any) {
- const rewritten =
- decodeURIComponent(parsedUrl.pathname) + 'index.html'
-
- if (fs.existsSync(path.join(root, rewritten))) {
- return rewritten
- }
-
- return spaFallback ? `/index.html` : request.url
- },
- },
- {
- from: /\.html$/,
- to({ parsedUrl, request }: any) {
- // .html files are not handled by serveStaticMiddleware
- // so we need to check if the file exists
- const pathname = decodeURIComponent(parsedUrl.pathname)
- if (fs.existsSync(path.join(root, pathname))) {
- return request.url
- }
- return spaFallback ? `/index.html` : request.url
- },
- },
- ],
- })
+ // When this middleware is mounted on a route, we need to re-assign `req.url` with a
+ // leading `.` to signal a relative rewrite. Returning with a leading `/` returns a
+ // buggy `req.url`. e.g.:
+ //
+ // mount /foo/bar:
+ // req.url = /index.html
+ // final = /foo/barindex.html
+ //
+ // mount /foo/bar:
+ // req.url = ./index.html
+ // final = /foo/bar/index.html
+ const prepend = mounted ? '.' : ''
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return function viteHtmlFallbackMiddleware(req, res, next) {
- return historyHtmlFallbackMiddleware(req, res, next)
+ if (
+ // Only accept GET or HEAD
+ (req.method !== 'GET' && req.method !== 'HEAD') ||
+ // Require Accept header
+ !req.headers ||
+ typeof req.headers.accept !== 'string' ||
+ // Ignore JSON requests
+ req.headers.accept.includes('application/json') ||
+ // Require Accept: text/html or */*
+ !(
+ req.headers.accept.includes('text/html') ||
+ req.headers.accept.includes('*/*')
+ )
+ ) {
+ return next()
+ }
+
+ const url = cleanUrl(req.url!)
+ const pathname = decodeURIComponent(url)
+
+ // .html files are not handled by serveStaticMiddleware
+ // so we need to check if the file exists
+ if (pathname.endsWith('.html')) {
+ const filePath = path.join(root, pathname)
+ if (fs.existsSync(filePath)) {
+ debug?.(`Rewriting ${req.method} ${req.url} to ${url}`)
+ req.url = prepend + url
+ return next()
+ }
+ }
+ // trailing slash should check for fallback index.html
+ else if (pathname[pathname.length - 1] === '/') {
+ const filePath = path.join(root, pathname, 'index.html')
+ if (fs.existsSync(filePath)) {
+ const newUrl = url + 'index.html'
+ debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`)
+ req.url = prepend + newUrl
+ return next()
+ }
+ }
+ // non-trailing slash should check for fallback .html
+ else {
+ const filePath = path.join(root, pathname + '.html')
+ if (fs.existsSync(filePath)) {
+ const newUrl = url + '.html'
+ debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`)
+ req.url = prepend + newUrl
+ return next()
+ }
+ }
+
+ if (spaFallback) {
+ debug?.(`Rewriting ${req.method} ${req.url} to /index.html`)
+ req.url = prepend + '/index.html'
+ }
+
+ next()
}
}
diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts
index c25aaa3f02e3ee..874c94f84790fa 100644
--- a/packages/vite/src/node/server/middlewares/indexHtml.ts
+++ b/packages/vite/src/node/server/middlewares/indexHtml.ts
@@ -23,7 +23,7 @@ import {
resolveHtmlTransforms,
traverseHtml,
} from '../../plugins/html'
-import type { ResolvedConfig, ViteDevServer } from '../..'
+import type { PreviewServer, ResolvedConfig, ViteDevServer } from '../..'
import { send } from '../send'
import { CLIENT_PUBLIC_PATH, FS_PREFIX } from '../../constants'
import {
@@ -32,6 +32,7 @@ import {
fsPathFromId,
getHash,
injectQuery,
+ isDevServer,
isJSRequest,
joinUrlSegments,
normalizePath,
@@ -378,8 +379,14 @@ const devHtmlHook: IndexHtmlTransformHook = async (
}
export function indexHtmlMiddleware(
- server: ViteDevServer,
+ root: string,
+ server: ViteDevServer | PreviewServer,
): Connect.NextHandleFunction {
+ const isDev = isDevServer(server)
+ const headers = isDev
+ ? server.config.server.headers
+ : server.config.preview.headers
+
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return async function viteIndexHtmlMiddleware(req, res, next) {
if (res.writableEnded) {
@@ -389,14 +396,20 @@ export function indexHtmlMiddleware(
const url = req.url && cleanUrl(req.url)
// htmlFallbackMiddleware appends '.html' to URLs
if (url?.endsWith('.html') && req.headers['sec-fetch-dest'] !== 'script') {
- const filename = getHtmlFilename(url, server)
- if (fs.existsSync(filename)) {
+ let filePath: string
+ if (isDev && url.startsWith(FS_PREFIX)) {
+ filePath = decodeURIComponent(fsPathFromId(url))
+ } else {
+ filePath = path.join(root, decodeURIComponent(url))
+ }
+
+ if (fs.existsSync(filePath)) {
try {
- let html = await fsp.readFile(filename, 'utf-8')
- html = await server.transformIndexHtml(url, html, req.originalUrl)
- return send(req, res, html, 'html', {
- headers: server.config.server.headers,
- })
+ let html = await fsp.readFile(filePath, 'utf-8')
+ if (isDev) {
+ html = await server.transformIndexHtml(url, html, req.originalUrl)
+ }
+ return send(req, res, html, 'html', { headers })
} catch (e) {
return next(e)
}
diff --git a/packages/vite/src/node/server/middlewares/notFound.ts b/packages/vite/src/node/server/middlewares/notFound.ts
new file mode 100644
index 00000000000000..4ecf6823dcaf89
--- /dev/null
+++ b/packages/vite/src/node/server/middlewares/notFound.ts
@@ -0,0 +1,9 @@
+import type { Connect } from 'dep-types/connect'
+
+export function notFoundMiddleware(): Connect.NextHandleFunction {
+ // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
+ return function vite404Middleware(_, res) {
+ res.statusCode = 404
+ res.end()
+ }
+}
diff --git a/packages/vite/src/node/shortcuts.ts b/packages/vite/src/node/shortcuts.ts
index 0a382c25b93c0c..8f8205d8e8bddd 100644
--- a/packages/vite/src/node/shortcuts.ts
+++ b/packages/vite/src/node/shortcuts.ts
@@ -2,6 +2,7 @@ import readline from 'node:readline'
import colors from 'picocolors'
import { restartServerWithUrls } from './server'
import type { ViteDevServer } from './server'
+import { isDevServer } from './utils'
import type { PreviewServer } from './preview'
import { openBrowser } from './server/openBrowser'
@@ -86,12 +87,6 @@ export function bindCLIShortcutsindex.html (fallback)
diff --git a/playground/html/serve/both.html b/playground/html/serve/both.html new file mode 100644 index 00000000000000..9eebf466b653b5 --- /dev/null +++ b/playground/html/serve/both.html @@ -0,0 +1 @@ +