diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index dfaab8362784c5..ec9173e18afed4 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -35,6 +35,7 @@ import { joinUrlSegments, normalizePath, requireResolveFromRootWithFallback, + withTrailingSlash, } from './utils' import { manifestPlugin } from './plugins/manifest' import type { Logger } from './logger' @@ -714,7 +715,7 @@ function prepareOutDir( for (const outDir of nonDuplicateDirs) { if ( fs.existsSync(outDir) && - !normalizePath(outDir).startsWith(config.root + '/') + !normalizePath(outDir).startsWith(withTrailingSlash(config.root)) ) { // warn if outDir is outside of root config.logger.warn( @@ -1240,5 +1241,9 @@ export const toOutputFilePathInHtml = toOutputFilePathWithoutRuntime function areSeparateFolders(a: string, b: string) { const na = normalizePath(a) const nb = normalizePath(b) - return na !== nb && !na.startsWith(nb + '/') && !nb.startsWith(na + '/') + return ( + na !== nb && + !na.startsWith(withTrailingSlash(nb)) && + !nb.startsWith(withTrailingSlash(na)) + ) } diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index f73ae2ae703e1c..b85788422672c1 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -39,6 +39,7 @@ import { mergeConfig, normalizeAlias, normalizePath, + withTrailingSlash, } from './utils' import { createPluginHookUtils, @@ -680,7 +681,7 @@ export async function resolveConfig( ), inlineConfig, root: resolvedRoot, - base: resolvedBase.endsWith('/') ? resolvedBase : resolvedBase + '/', + base: withTrailingSlash(resolvedBase), rawBase: resolvedBase, resolve: resolveOptions, publicDir: resolvedPublicDir, @@ -856,7 +857,7 @@ assetFileNames isn't equal for every build.rollupOptions.output. A single patter ) { resolved.logger.warn( colors.yellow(` -(!) Experimental legacy.buildSsrCjsExternalHeuristics and ssr.format: 'cjs' are going to be removed in Vite 5. +(!) Experimental legacy.buildSsrCjsExternalHeuristics and ssr.format: 'cjs' are going to be removed in Vite 5. Find more information and give feedback at https://github.com/vitejs/vite/discussions/13816. `), ) diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 2c298a94c31f5a..d16130b58c01b9 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -23,6 +23,7 @@ import { joinUrlSegments, normalizePath, removeLeadingSlash, + withTrailingSlash, } from '../utils' import { FS_PREFIX } from '../constants' @@ -229,7 +230,11 @@ export function checkPublicFile( return } const publicFile = path.join(publicDir, cleanUrl(url)) - if (!publicFile.startsWith(publicDir)) { + if ( + !normalizePath(publicFile).startsWith( + withTrailingSlash(normalizePath(publicDir)), + ) + ) { // can happen if URL starts with '../' return } @@ -257,7 +262,7 @@ function fileToDevUrl(id: string, config: ResolvedConfig) { if (checkPublicFile(id, config)) { // in public dir, keep the url as-is rtn = id - } else if (id.startsWith(config.root)) { + } else if (id.startsWith(withTrailingSlash(config.root))) { // in project root, infer short public path rtn = '/' + path.posix.relative(config.root, id) } else { diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 620fa38175d19d..814aa9187a9ce0 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -44,6 +44,7 @@ import { timeFrom, transformStableResult, unwrapId, + withTrailingSlash, wrapId, } from '../utils' import { getDepOptimizationConfig } from '../config' @@ -335,7 +336,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // normalize all imports into resolved URLs // e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js'` - if (resolved.id.startsWith(root + '/')) { + if (resolved.id.startsWith(withTrailingSlash(root))) { // in root: infer short absolute path from root url = resolved.id.slice(root.length) } else if ( @@ -672,7 +673,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { config.logger.error(e.message, { error: e }) }) } - } else if (!importer.startsWith(clientDir)) { + } else if (!importer.startsWith(withTrailingSlash(clientDir))) { if (!isInNodeModules(importer)) { // check @vite-ignore which suppresses dynamic import warning const hasViteIgnore = hasViteIgnoreRE.test( diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 1d046eb49f581e..1889790463dae8 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -16,6 +16,7 @@ import { isInNodeModules, moduleListContains, numberToPos, + withTrailingSlash, } from '../utils' import type { Plugin } from '../plugin' import { getDepOptimizationConfig } from '../config' @@ -271,7 +272,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { // normalize all imports into resolved URLs // e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js'` - if (resolved.id.startsWith(root + '/')) { + if (resolved.id.startsWith(withTrailingSlash(root))) { // in root: infer short absolute path from root url = resolved.id.slice(root.length) } else { diff --git a/packages/vite/src/node/plugins/preAlias.ts b/packages/vite/src/node/plugins/preAlias.ts index 931db79d231bf7..cd86bac770d823 100644 --- a/packages/vite/src/node/plugins/preAlias.ts +++ b/packages/vite/src/node/plugins/preAlias.ts @@ -14,6 +14,7 @@ import { isInNodeModules, isOptimizable, moduleListContains, + withTrailingSlash, } from '../utils' import { getDepsOptimizer } from '../optimizer' import { tryOptimizedResolve } from './resolve' @@ -114,7 +115,7 @@ function matches(pattern: string | RegExp, importee: string) { if (importee === pattern) { return true } - return importee.startsWith(pattern + '/') + return importee.startsWith(withTrailingSlash(pattern)) } function getAliasPatterns( diff --git a/packages/vite/src/node/plugins/reporter.ts b/packages/vite/src/node/plugins/reporter.ts index cbcb0409d1e3f0..74ffe11246e8ad 100644 --- a/packages/vite/src/node/plugins/reporter.ts +++ b/packages/vite/src/node/plugins/reporter.ts @@ -4,7 +4,12 @@ import { promisify } from 'node:util' import colors from 'picocolors' import type { Plugin } from 'rollup' import type { ResolvedConfig } from '../config' -import { isDefined, isInNodeModules, normalizePath } from '../utils' +import { + isDefined, + isInNodeModules, + normalizePath, + withTrailingSlash, +} from '../utils' import { LogLevels } from '../logger' const groups = [ @@ -243,9 +248,10 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { group.name === 'JS' && entry.size / 1000 > chunkLimit if (isLarge) hasLargeChunks = true const sizeColor = isLarge ? colors.yellow : colors.dim - let log = colors.dim(relativeOutDir + '/') + let log = colors.dim(withTrailingSlash(relativeOutDir)) log += - !config.build.lib && entry.name.startsWith(assetsDir) + !config.build.lib && + entry.name.startsWith(withTrailingSlash(assetsDir)) ? colors.dim(assetsDir) + group.color( entry.name diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 63841d398c5256..26c30b9a9c9edc 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -36,6 +36,7 @@ import { safeRealpathSync, slash, tryStatSync, + withTrailingSlash, } from '../utils' import { optimizedDepInfoFromFile, optimizedDepInfoFromId } from '../optimizer' import type { DepsOptimizer } from '../optimizer' @@ -228,7 +229,11 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { // URL // /foo -> /fs-root/foo - if (asSrc && id[0] === '/' && (rootInRoot || !id.startsWith(root))) { + if ( + asSrc && + id[0] === '/' && + (rootInRoot || !id.startsWith(withTrailingSlash(root))) + ) { const fsPath = path.resolve(root, id.slice(1)) if ((res = tryFsResolve(fsPath, options))) { debug?.(`[url] ${colors.cyan(id)} -> ${colors.dim(res)}`) @@ -939,7 +944,7 @@ export async function tryOptimizedResolve( } // match by src to correctly identify if id belongs to nested dependency - if (optimizedData.src.startsWith(idPkgDir)) { + if (optimizedData.src.startsWith(withTrailingSlash(idPkgDir))) { return depsOptimizer.getOptimizedDepId(optimizedData) } } diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index e72b1ebdf05338..f708b1114ed3cd 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -5,7 +5,13 @@ import colors from 'picocolors' import type { Update } from 'types/hmrPayload' import type { RollupError } from 'rollup' import { CLIENT_DIR } from '../constants' -import { createDebugger, normalizePath, unique, wrapId } from '../utils' +import { + createDebugger, + normalizePath, + unique, + withTrailingSlash, + wrapId, +} from '../utils' import type { ViteDevServer } from '..' import { isCSSRequest } from '../plugins/css' import { getAffectedGlobModules } from '../plugins/importMetaGlob' @@ -38,7 +44,9 @@ export interface HmrContext { } export function getShortName(file: string, root: string): string { - return file.startsWith(root + '/') ? path.posix.relative(root, file) : file + return file.startsWith(withTrailingSlash(root)) + ? path.posix.relative(root, file) + : file } export async function handleHMRUpdate( @@ -81,7 +89,7 @@ export async function handleHMRUpdate( debugHmr?.(`[file change] ${colors.dim(shortFile)}`) // (dev only) the client itself cannot be hot updated. - if (file.startsWith(normalizedClientDir)) { + if (file.startsWith(withTrailingSlash(normalizedClientDir))) { ws.send({ type: 'full-reload', path: '*', diff --git a/packages/vite/src/node/server/middlewares/base.ts b/packages/vite/src/node/server/middlewares/base.ts index cba4582486f8a9..c6af2302aef3d9 100644 --- a/packages/vite/src/node/server/middlewares/base.ts +++ b/packages/vite/src/node/server/middlewares/base.ts @@ -1,6 +1,6 @@ import type { Connect } from 'dep-types/connect' import type { ViteDevServer } from '..' -import { joinUrlSegments, stripBase } from '../../utils' +import { joinUrlSegments, stripBase, withTrailingSlash } from '../../utils' // this middleware is only active when (base !== '/') @@ -36,7 +36,7 @@ export function baseMiddleware({ } else if (req.headers.accept?.includes('text/html')) { // non-based page visit const redirectPath = - url + '/' !== base ? joinUrlSegments(base, url) : base + withTrailingSlash(url) !== base ? joinUrlSegments(base, url) : base res.writeHead(404, { 'Content-Type': 'text/html', }) diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts index d1e7073179415b..8acc9e681b4e37 100644 --- a/packages/vite/src/node/server/middlewares/static.ts +++ b/packages/vite/src/node/server/middlewares/static.ts @@ -19,6 +19,7 @@ import { removeLeadingSlash, shouldServeFile, slash, + withTrailingSlash, } from '../../utils' const knownJavascriptExtensionRE = /\.[tj]sx?$/ @@ -118,7 +119,7 @@ export function serveStaticMiddleware( } if (redirectedPathname) { // dir is pre-normalized to posix style - if (redirectedPathname.startsWith(dir)) { + if (redirectedPathname.startsWith(withTrailingSlash(dir))) { redirectedPathname = redirectedPathname.slice(dir.length) } } @@ -129,7 +130,7 @@ export function serveStaticMiddleware( resolvedPathname[resolvedPathname.length - 1] === '/' && fileUrl[fileUrl.length - 1] !== '/' ) { - fileUrl = fileUrl + '/' + fileUrl = withTrailingSlash(fileUrl) } if (!ensureServingAccess(fileUrl, server, res, next)) { return diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index 737fb92f7d0fbc..f85b12a46202f4 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -16,6 +16,7 @@ import { removeImportQuery, removeTimestampQuery, unwrapId, + withTrailingSlash, } from '../../utils' import { send } from '../send' import { ERR_LOAD_URL, transformRequest } from '../transformRequest' @@ -129,10 +130,10 @@ export function transformMiddleware( // check if public dir is inside root dir const publicDir = normalizePath(server.config.publicDir) const rootDir = normalizePath(server.config.root) - if (publicDir.startsWith(rootDir)) { + if (publicDir.startsWith(withTrailingSlash(rootDir))) { const publicPath = `${publicDir.slice(rootDir.length)}/` // warn explicit public paths - if (url.startsWith(publicPath)) { + if (url.startsWith(withTrailingSlash(publicPath))) { let warning: string if (isImportRequest(url)) { diff --git a/packages/vite/src/node/ssr/ssrExternal.ts b/packages/vite/src/node/ssr/ssrExternal.ts index 9a5bc28b39b9ee..99001f3614f330 100644 --- a/packages/vite/src/node/ssr/ssrExternal.ts +++ b/packages/vite/src/node/ssr/ssrExternal.ts @@ -13,6 +13,7 @@ import { isInNodeModules, lookupFile, normalizePath, + withTrailingSlash, } from '../utils' import type { Logger, ResolvedConfig } from '..' import { resolvePackageData } from '../packages' @@ -340,7 +341,10 @@ export function cjsShouldExternalizeForSSR( } // deep imports, check ext before externalizing - only externalize // extension-less imports and explicit .js imports - if (id.startsWith(e + '/') && (!path.extname(id) || id.endsWith('.js'))) { + if ( + id.startsWith(withTrailingSlash(e)) && + (!path.extname(id) || id.endsWith('.js')) + ) { return true } }) diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 27a619947c0b0d..66d071dddc7bbd 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -123,7 +123,9 @@ export function moduleListContains( moduleList: string[] | undefined, id: string, ): boolean | undefined { - return moduleList?.some((m) => m === id || id.startsWith(m + '/')) + return moduleList?.some( + (m) => m === id || id.startsWith(withTrailingSlash(m)), + ) } export function isOptimizable( @@ -221,6 +223,13 @@ export function fsPathFromUrl(url: string): string { return fsPathFromId(cleanUrl(url)) } +export function withTrailingSlash(path: string): string { + if (path[path.length - 1] !== '/') { + return `${path}/` + } + return path +} + /** * Check if dir is a parent of file * @@ -231,9 +240,7 @@ export function fsPathFromUrl(url: string): string { * @returns true if dir is a parent of file */ export function isParentDirectory(dir: string, file: string): boolean { - if (dir[dir.length - 1] !== '/') { - dir = `${dir}/` - } + dir = withTrailingSlash(dir) return ( file.startsWith(dir) || (isCaseInsensitiveFS && file.toLowerCase().startsWith(dir.toLowerCase())) @@ -644,7 +651,7 @@ export function ensureWatchedFile( if ( file && // only need to watch if out of root - !file.startsWith(root + '/') && + !file.startsWith(withTrailingSlash(root)) && // some rollup plugins use null bytes for private resolved Ids !file.includes('\0') && fs.existsSync(file) @@ -1222,7 +1229,7 @@ export function stripBase(path: string, base: string): string { if (path === base) { return '/' } - const devBase = base[base.length - 1] === '/' ? base : base + '/' + const devBase = withTrailingSlash(base) return path.startsWith(devBase) ? path.slice(devBase.length - 1) : path }