diff --git a/docs/config/server-options.md b/docs/config/server-options.md index da75fc3a46a04a..89287fe53cf98a 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -242,7 +242,7 @@ createServer() - **Type:** `string | undefined` -Prepend this folder to http requests, for use when proxying vite as a subfolder. Should start and end with the `/` character. +Prepend this folder to http requests, for use when proxying vite as a subfolder. Should start with the `/` character. ## server.fs.strict diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 72b5948ccc04af..8ebe96ccd41a52 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -1138,7 +1138,7 @@ export function toOutputFilePathWithoutRuntime( if (relative && !config.build.ssr) { return toRelative(filename, hostId) } else { - return config.base + filename + return joinUrlSegments(config.base, filename) } } diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index f5cba176f5c246..5bfdea72654678 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -323,6 +323,8 @@ export type ResolvedConfig = Readonly< inlineConfig: InlineConfig root: string base: string + /** @internal */ + rawBase: string publicDir: string cacheDir: string command: 'build' | 'serve' @@ -626,7 +628,8 @@ export async function resolveConfig( ), inlineConfig, root: resolvedRoot, - base: resolvedBase, + base: resolvedBase.endsWith('/') ? resolvedBase : resolvedBase + '/', + rawBase: resolvedBase, resolve: resolveOptions, publicDir: resolvedPublicDir, cacheDir, @@ -819,12 +822,6 @@ export function resolveBaseUrl( colors.yellow(colors.bold(`(!) "base" option should start with a slash.`)) ) } - // no ending slash warn - if (!base.endsWith('/')) { - logger.warn( - colors.yellow(colors.bold(`(!) "base" option should end with a slash.`)) - ) - } // parse base when command is serve or base is not External URL if (!isBuild || !isExternal) { @@ -834,10 +831,6 @@ export function resolveBaseUrl( base = '/' + base } } - // ensure ending slash - if (!base.endsWith('/')) { - base += '/' - } return base } diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 9bb5d7f7a42d84..0d67af4fa0e05b 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -247,7 +247,7 @@ function fileToDevUrl(id: string, config: ResolvedConfig) { } else { // outside of project root, use absolute fs path // (this is special handled by the serve static middleware - rtn = path.posix.join(FS_PREFIX + id) + rtn = path.posix.join(FS_PREFIX, id) } const base = joinUrlSegments(config.server?.origin ?? '', config.base) return joinUrlSegments(base, rtn.replace(/^\//, '')) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index c14269ccf99734..caa7a36f80cbeb 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -46,6 +46,7 @@ import { processSrcSet, removeDirectQuery, requireResolveFromRootWithFallback, + stripBase, stripBomTag } from '../utils' import type { Logger } from '../logger' @@ -265,9 +266,10 @@ export function cssPlugin(config: ResolvedConfig): Plugin { isCSSRequest(file) ? moduleGraph.createFileOnlyEntry(file) : await moduleGraph.ensureEntryFromUrl( - ( - await fileToUrl(file, config, this) - ).replace((config.server?.origin ?? '') + devBase, '/'), + stripBase( + await fileToUrl(file, config, this), + (config.server?.origin ?? '') + devBase + ), ssr ) ) diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 80ee54e501a26e..d15496d6d34c17 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -33,10 +33,12 @@ import { isDataUrl, isExternalUrl, isJSRequest, + joinUrlSegments, moduleListContains, normalizePath, prettifyUrl, removeImportQuery, + stripBase, stripBomTag, timeFrom, transformStableResult, @@ -263,9 +265,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { url: string, pos: number ): Promise<[string, string]> => { - if (base !== '/' && url.startsWith(base)) { - url = url.replace(base, '/') - } + url = stripBase(url, base) let importerFile = importer @@ -319,7 +319,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { ) { // an optimized deps may not yet exists in the filesystem, or // a regular file exists but is out of root: rewrite to absolute /@fs/ paths - url = path.posix.join(FS_PREFIX + resolved.id) + url = path.posix.join(FS_PREFIX, resolved.id) } else { url = resolved.id } @@ -376,8 +376,8 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { throw e } - // prepend base (dev base is guaranteed to have ending slash) - url = base + url.replace(/^\//, '') + // prepend base + url = joinUrlSegments(base, url) } return [url, resolved.id] @@ -538,7 +538,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // record for HMR import chain analysis // make sure to unwrap and normalize away base - const hmrUrl = unwrapId(url.replace(base, '/')) + const hmrUrl = unwrapId(stripBase(url, base)) importedUrls.add(hmrUrl) if (enablePartialAccept && importedBindings) { diff --git a/packages/vite/src/node/server/middlewares/base.ts b/packages/vite/src/node/server/middlewares/base.ts index 93d7b4950323d9..08aa76577a7a6f 100644 --- a/packages/vite/src/node/server/middlewares/base.ts +++ b/packages/vite/src/node/server/middlewares/base.ts @@ -1,24 +1,23 @@ import type { Connect } from 'dep-types/connect' import type { ViteDevServer } from '..' -import { joinUrlSegments } from '../../utils' +import { joinUrlSegments, stripBase } from '../../utils' -// this middleware is only active when (config.base !== '/') +// this middleware is only active when (base !== '/') export function baseMiddleware({ config }: ViteDevServer): Connect.NextHandleFunction { - const devBase = config.base.endsWith('/') ? config.base : config.base + '/' - // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` return function viteBaseMiddleware(req, res, next) { const url = req.url! const parsed = new URL(url, 'http://vitejs.dev') const path = parsed.pathname || '/' + const base = config.rawBase - if (path.startsWith(devBase)) { + if (path.startsWith(base)) { // rewrite url to remove base. this ensures that other middleware does // not need to consider base being prepended or not - req.url = url.replace(devBase, '/') + req.url = stripBase(url, base) return next() } @@ -30,18 +29,19 @@ export function baseMiddleware({ if (path === '/' || path === '/index.html') { // redirect root visit to based url with search and hash res.writeHead(302, { - Location: config.base + (parsed.search || '') + (parsed.hash || '') + Location: base + (parsed.search || '') + (parsed.hash || '') }) res.end() return } else if (req.headers.accept?.includes('text/html')) { // non-based page visit - const redirectPath = joinUrlSegments(config.base, url) + const redirectPath = + url + '/' !== base ? joinUrlSegments(base, url) : base res.writeHead(404, { 'Content-Type': 'text/html' }) res.end( - `The server is configured with a public base URL of ${config.base} - ` + + `The server is configured with a public base URL of ${base} - ` + `did you mean to visit ${redirectPath} instead?` ) return diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 3c1e9c90f2a034..379e633e14591a 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -868,7 +868,8 @@ export async function resolveServerUrls( const hostname = await resolveHostname(options.host) const protocol = options.https ? 'https' : 'http' const port = address.port - const base = config.base === './' || config.base === '' ? '/' : config.base + const base = + config.rawBase === './' || config.rawBase === '' ? '/' : config.rawBase if (hostname.host && loopbackHosts.has(hostname.host)) { let hostnameName = hostname.name @@ -1250,6 +1251,14 @@ export function joinUrlSegments(a: string, b: string): string { return a + b } +export function stripBase(path: string, base: string): string { + if (path === base) { + return '/' + } + const devBase = base.endsWith('/') ? base : base + '/' + return path.replace(RegExp('^' + devBase), '/') +} + export function arrayEqual(a: any[], b: any[]): boolean { if (a === b) return true if (a.length !== b.length) return false diff --git a/playground/assets/__tests__/assets.spec.ts b/playground/assets/__tests__/assets.spec.ts index f8246a4f3418b3..3c20dc96246574 100644 --- a/playground/assets/__tests__/assets.spec.ts +++ b/playground/assets/__tests__/assets.spec.ts @@ -1,3 +1,4 @@ +import path from 'node:path' import fetch from 'node-fetch' import { describe, expect, test } from 'vitest' import { @@ -30,8 +31,12 @@ 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) + expect((await fetch(path.posix.join(viteTestUrl, 'icon.png'))).status).toBe( + 200 + ) + expect((await fetch(path.posix.join(viteTestUrl, 'ICON.png'))).status).toBe( + 404 + ) }) describe('injected scripts', () => { @@ -312,7 +317,7 @@ test('new URL(`${dynamic}`, import.meta.url)', async () => { test('new URL(`non-existent`, import.meta.url)', async () => { expect(await page.textContent('.non-existent-import-meta-url')).toMatch( - '/foo/non-existent' + new URL('non-existent', page.url()).pathname ) }) diff --git a/playground/assets/package.json b/playground/assets/package.json index d3b685dbed678b..6d28ed91719070 100644 --- a/playground/assets/package.json +++ b/playground/assets/package.json @@ -3,9 +3,9 @@ "private": true, "version": "0.0.0", "scripts": { + "debug": "node --inspect-brk ../../packages/vite/bin/vite", "dev": "vite", "build": "vite build", - "debug": "node --inspect-brk ../../packages/vite/bin/vite", "preview": "vite preview", "dev:relative-base": "vite --config ./vite.config-relative-base.js dev", "build:relative-base": "vite --config ./vite.config-relative-base.js build", diff --git a/playground/assets/vite.config.js b/playground/assets/vite.config.js index 700896cc13d50b..ff4789679659d8 100644 --- a/playground/assets/vite.config.js +++ b/playground/assets/vite.config.js @@ -4,7 +4,7 @@ const path = require('node:path') * @type {import('vite').UserConfig} */ module.exports = { - base: '/foo/', + base: '/foo', publicDir: 'static', resolve: { alias: {