diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 65563735927d0f..fe147c1cd1b48d 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -27,7 +27,13 @@ import { isDepsOptimizerEnabled, resolveConfig } from './config' import { buildReporterPlugin } from './plugins/reporter' import { buildEsbuildPlugin } from './plugins/esbuild' import { terserPlugin } from './plugins/terser' -import { copyDir, emptyDir, lookupFile, normalizePath } from './utils' +import { + copyDir, + emptyDir, + joinUrlSegments, + lookupFile, + normalizePath +} from './utils' import { manifestPlugin } from './plugins/manifest' import type { Logger } from './logger' import { dataURIPlugin } from './plugins/dataUri' @@ -1071,7 +1077,7 @@ export function toOutputFilePathInJS( if (relative && !config.build.ssr) { return toRelative(filename, hostId) } - return config.base + filename + return joinUrlSegments(config.base, filename) } export function createToImportMetaURLBasedRelativeRuntime( diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index a7a8f2554bf0a1..8a74736715bd9c 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -19,7 +19,7 @@ import { } from '../build' import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' -import { cleanUrl, getHash, normalizePath } from '../utils' +import { cleanUrl, getHash, joinUrlSegments, normalizePath } from '../utils' import { FS_PREFIX } from '../constants' export const assetUrlRE = /__VITE_ASSET__([a-z\d]{8})__(?:\$_(.*?)__)?/g @@ -249,9 +249,8 @@ function fileToDevUrl(id: string, config: ResolvedConfig) { // (this is special handled by the serve static middleware rtn = path.posix.join(FS_PREFIX + id) } - const origin = config.server?.origin ?? '' - const devBase = config.base - return origin + devBase + rtn.replace(/^\//, '') + const base = joinUrlSegments(config.server?.origin ?? '', config.base) + return joinUrlSegments(base, rtn.replace(/^\//, '')) } export function getAssetFilename( @@ -396,7 +395,7 @@ export function publicFileToBuiltUrl( ): string { if (config.command !== 'build') { // We don't need relative base or renderBuiltUrl support during dev - return config.base + url.slice(1) + return joinUrlSegments(config.base, url) } const hash = getHash(url) let cache = publicAssetUrlCache.get(config) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 09b70bbb0391aa..ad46e5ba738f37 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -39,6 +39,7 @@ import { isDataUrl, isExternalUrl, isObject, + joinUrlSegments, normalizePath, parseRequest, processSrcSet, @@ -211,7 +212,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { if (encodePublicUrlsInCSS(config)) { return publicFileToBuiltUrl(url, config) } else { - return config.base + url.slice(1) + return joinUrlSegments(config.base, url) } } const resolved = await resolveUrl(url, importer) @@ -249,7 +250,6 @@ export function cssPlugin(config: ResolvedConfig): Plugin { // server only logic for handling CSS @import dependency hmr const { moduleGraph } = server const thisModule = moduleGraph.getModuleById(id) - const devBase = config.base if (thisModule) { // CSS modules cannot self-accept since it exports values const isSelfAccepting = @@ -258,6 +258,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { // record deps in the module graph so edits to @import css can trigger // main import to hot update const depModules = new Set() + const devBase = config.base for (const file of deps) { depModules.add( isCSSRequest(file) @@ -387,10 +388,9 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { } const cssContent = await getContentWithSourcemap(css) - const devBase = config.base const code = [ `import { updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle } from ${JSON.stringify( - path.posix.join(devBase, CLIENT_PUBLIC_PATH) + path.posix.join(config.base, CLIENT_PUBLIC_PATH) )}`, `const __vite__id = ${JSON.stringify(id)}`, `const __vite__css = ${JSON.stringify(cssContent)}`, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 5e8296e5ad3aad..7ea1d846183bb5 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -547,8 +547,7 @@ export async function createServer( } // base - const devBase = config.base - if (devBase !== '/') { + if (config.base !== '/') { middlewares.use(baseMiddleware(server)) } @@ -652,7 +651,6 @@ async function startServer( const protocol = options.https ? 'https' : 'http' const info = server.config.logger.info - const devBase = server.config.base const serverPort = await httpServerStart(httpServer, { port, @@ -681,7 +679,8 @@ async function startServer( } if (options.open && !isRestart) { - const path = typeof options.open === 'string' ? options.open : devBase + const path = + typeof options.open === 'string' ? options.open : server.config.base openBrowser( path.startsWith('http') ? path diff --git a/packages/vite/src/node/server/middlewares/base.ts b/packages/vite/src/node/server/middlewares/base.ts index 27960f900b44b7..93d7b4950323d9 100644 --- a/packages/vite/src/node/server/middlewares/base.ts +++ b/packages/vite/src/node/server/middlewares/base.ts @@ -1,12 +1,13 @@ import type { Connect } from 'dep-types/connect' import type { ViteDevServer } from '..' +import { joinUrlSegments } from '../../utils' // this middleware is only active when (config.base !== '/') export function baseMiddleware({ config }: ViteDevServer): Connect.NextHandleFunction { - const devBase = config.base + 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) { @@ -29,18 +30,18 @@ export function baseMiddleware({ if (path === '/' || path === '/index.html') { // redirect root visit to based url with search and hash res.writeHead(302, { - Location: devBase + (parsed.search || '') + (parsed.hash || '') + Location: config.base + (parsed.search || '') + (parsed.hash || '') }) res.end() return } else if (req.headers.accept?.includes('text/html')) { // non-based page visit - const redirectPath = devBase + url.slice(1) + const redirectPath = joinUrlSegments(config.base, url) res.writeHead(404, { 'Content-Type': 'text/html' }) res.end( - `The server is configured with a public base URL of ${devBase} - ` + + `The server is configured with a public base URL of ${config.base} - ` + `did you mean to visit ${redirectPath} instead?` ) return diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index 326281b853f079..3c57832db0fd20 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -26,6 +26,7 @@ import { ensureWatchedFile, fsPathFromId, injectQuery, + joinUrlSegments, normalizePath, processSrcSetSync, wrapId @@ -93,7 +94,8 @@ const processNodeUrl = ( const devBase = config.base if (startsWithSingleSlashRE.test(url)) { // prefix with base (dev only, base is never relative) - overwriteAttrValue(s, sourceCodeLocation, devBase + url.slice(1)) + const fullUrl = joinUrlSegments(devBase, url) + overwriteAttrValue(s, sourceCodeLocation, fullUrl) } else if ( url.startsWith('.') && originalUrl && @@ -132,7 +134,7 @@ const devHtmlHook: IndexHtmlTransformHook = async ( const trailingSlash = htmlPath.endsWith('/') if (!trailingSlash && fs.existsSync(filename)) { proxyModulePath = htmlPath - proxyModuleUrl = base + htmlPath.slice(1) + proxyModuleUrl = joinUrlSegments(base, htmlPath) } else { // There are users of vite.transformIndexHtml calling it with url '/' // for SSR integrations #7993, filename is root for this case diff --git a/packages/vite/src/node/ssr/ssrManifestPlugin.ts b/packages/vite/src/node/ssr/ssrManifestPlugin.ts index 1cc2d79d770fe8..9a68b5ea22afe5 100644 --- a/packages/vite/src/node/ssr/ssrManifestPlugin.ts +++ b/packages/vite/src/node/ssr/ssrManifestPlugin.ts @@ -5,7 +5,7 @@ import type { OutputChunk } from 'rollup' import type { ResolvedConfig } from '..' import type { Plugin } from '../plugin' import { preloadMethod } from '../plugins/importAnalysisBuild' -import { normalizePath } from '../utils' +import { joinUrlSegments, normalizePath } from '../utils' export function ssrManifestPlugin(config: ResolvedConfig): Plugin { // module id => preload assets mapping @@ -23,15 +23,15 @@ export function ssrManifestPlugin(config: ResolvedConfig): Plugin { const mappedChunks = ssrManifest[normalizedId] ?? (ssrManifest[normalizedId] = []) if (!chunk.isEntry) { - mappedChunks.push(base + chunk.fileName) + mappedChunks.push(joinUrlSegments(base, chunk.fileName)) // tags for entry chunks are already generated in static HTML, // so we only need to record info for non-entry chunks. chunk.viteMetadata.importedCss.forEach((file) => { - mappedChunks.push(base + file) + mappedChunks.push(joinUrlSegments(base, file)) }) } chunk.viteMetadata.importedAssets.forEach((file) => { - mappedChunks.push(base + file) + mappedChunks.push(joinUrlSegments(base, file)) }) } if (chunk.code.includes(preloadMethod)) { @@ -59,7 +59,7 @@ export function ssrManifestPlugin(config: ResolvedConfig): Plugin { const chunk = bundle[filename] as OutputChunk | undefined if (chunk) { chunk.viteMetadata.importedCss.forEach((file) => { - deps.push(join(base, file)) // TODO:base + deps.push(joinUrlSegments(base, file)) // TODO:base }) chunk.imports.forEach(addDeps) } diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 5e7081813a8c06..bead9cee598504 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -1191,3 +1191,16 @@ export const isNonDriveRelativeAbsolutePath = (p: string): boolean => { if (!isWindows) return p.startsWith('/') return windowsDrivePathPrefixRE.test(p) } + +export function joinUrlSegments(a: string, b: string): string { + if (!a || !b) { + return a || b || '' + } + if (a.endsWith('/')) { + a = a.substring(0, a.length - 1) + } + if (!b.startsWith('/')) { + b = '/' + b + } + return a + b +}