Skip to content

Commit 8f87282

Browse files
authoredNov 12, 2022
feat: base without trailing slash (#10723)
Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Closes #9236 Closes #8770 Closes #8772
1 parent ce24c7c commit 8f87282

File tree

11 files changed

+48
-39
lines changed

11 files changed

+48
-39
lines changed
 

‎docs/config/server-options.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ createServer()
242242

243243
- **Type:** `string | undefined`
244244

245-
Prepend this folder to http requests, for use when proxying vite as a subfolder. Should start and end with the `/` character.
245+
Prepend this folder to http requests, for use when proxying vite as a subfolder. Should start with the `/` character.
246246

247247
## server.fs.strict
248248

‎packages/vite/src/node/build.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1138,7 +1138,7 @@ export function toOutputFilePathWithoutRuntime(
11381138
if (relative && !config.build.ssr) {
11391139
return toRelative(filename, hostId)
11401140
} else {
1141-
return config.base + filename
1141+
return joinUrlSegments(config.base, filename)
11421142
}
11431143
}
11441144

‎packages/vite/src/node/config.ts

+4-11
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,8 @@ export type ResolvedConfig = Readonly<
323323
inlineConfig: InlineConfig
324324
root: string
325325
base: string
326+
/** @internal */
327+
rawBase: string
326328
publicDir: string
327329
cacheDir: string
328330
command: 'build' | 'serve'
@@ -626,7 +628,8 @@ export async function resolveConfig(
626628
),
627629
inlineConfig,
628630
root: resolvedRoot,
629-
base: resolvedBase,
631+
base: resolvedBase.endsWith('/') ? resolvedBase : resolvedBase + '/',
632+
rawBase: resolvedBase,
630633
resolve: resolveOptions,
631634
publicDir: resolvedPublicDir,
632635
cacheDir,
@@ -819,12 +822,6 @@ export function resolveBaseUrl(
819822
colors.yellow(colors.bold(`(!) "base" option should start with a slash.`))
820823
)
821824
}
822-
// no ending slash warn
823-
if (!base.endsWith('/')) {
824-
logger.warn(
825-
colors.yellow(colors.bold(`(!) "base" option should end with a slash.`))
826-
)
827-
}
828825

829826
// parse base when command is serve or base is not External URL
830827
if (!isBuild || !isExternal) {
@@ -834,10 +831,6 @@ export function resolveBaseUrl(
834831
base = '/' + base
835832
}
836833
}
837-
// ensure ending slash
838-
if (!base.endsWith('/')) {
839-
base += '/'
840-
}
841834

842835
return base
843836
}

‎packages/vite/src/node/plugins/asset.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ function fileToDevUrl(id: string, config: ResolvedConfig) {
247247
} else {
248248
// outside of project root, use absolute fs path
249249
// (this is special handled by the serve static middleware
250-
rtn = path.posix.join(FS_PREFIX + id)
250+
rtn = path.posix.join(FS_PREFIX, id)
251251
}
252252
const base = joinUrlSegments(config.server?.origin ?? '', config.base)
253253
return joinUrlSegments(base, rtn.replace(/^\//, ''))

‎packages/vite/src/node/plugins/css.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
processSrcSet,
4747
removeDirectQuery,
4848
requireResolveFromRootWithFallback,
49+
stripBase,
4950
stripBomTag
5051
} from '../utils'
5152
import type { Logger } from '../logger'
@@ -265,9 +266,10 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
265266
isCSSRequest(file)
266267
? moduleGraph.createFileOnlyEntry(file)
267268
: await moduleGraph.ensureEntryFromUrl(
268-
(
269-
await fileToUrl(file, config, this)
270-
).replace((config.server?.origin ?? '') + devBase, '/'),
269+
stripBase(
270+
await fileToUrl(file, config, this),
271+
(config.server?.origin ?? '') + devBase
272+
),
271273
ssr
272274
)
273275
)

‎packages/vite/src/node/plugins/importAnalysis.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ import {
3333
isDataUrl,
3434
isExternalUrl,
3535
isJSRequest,
36+
joinUrlSegments,
3637
moduleListContains,
3738
normalizePath,
3839
prettifyUrl,
3940
removeImportQuery,
41+
stripBase,
4042
stripBomTag,
4143
timeFrom,
4244
transformStableResult,
@@ -263,9 +265,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
263265
url: string,
264266
pos: number
265267
): Promise<[string, string]> => {
266-
if (base !== '/' && url.startsWith(base)) {
267-
url = url.replace(base, '/')
268-
}
268+
url = stripBase(url, base)
269269

270270
let importerFile = importer
271271

@@ -319,7 +319,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
319319
) {
320320
// an optimized deps may not yet exists in the filesystem, or
321321
// a regular file exists but is out of root: rewrite to absolute /@fs/ paths
322-
url = path.posix.join(FS_PREFIX + resolved.id)
322+
url = path.posix.join(FS_PREFIX, resolved.id)
323323
} else {
324324
url = resolved.id
325325
}
@@ -376,8 +376,8 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
376376
throw e
377377
}
378378

379-
// prepend base (dev base is guaranteed to have ending slash)
380-
url = base + url.replace(/^\//, '')
379+
// prepend base
380+
url = joinUrlSegments(base, url)
381381
}
382382

383383
return [url, resolved.id]
@@ -538,7 +538,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
538538

539539
// record for HMR import chain analysis
540540
// make sure to unwrap and normalize away base
541-
const hmrUrl = unwrapId(url.replace(base, '/'))
541+
const hmrUrl = unwrapId(stripBase(url, base))
542542
importedUrls.add(hmrUrl)
543543

544544
if (enablePartialAccept && importedBindings) {

‎packages/vite/src/node/server/middlewares/base.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
import type { Connect } from 'dep-types/connect'
22
import type { ViteDevServer } from '..'
3-
import { joinUrlSegments } from '../../utils'
3+
import { joinUrlSegments, stripBase } from '../../utils'
44

5-
// this middleware is only active when (config.base !== '/')
5+
// this middleware is only active when (base !== '/')
66

77
export function baseMiddleware({
88
config
99
}: ViteDevServer): Connect.NextHandleFunction {
10-
const devBase = config.base.endsWith('/') ? config.base : config.base + '/'
11-
1210
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
1311
return function viteBaseMiddleware(req, res, next) {
1412
const url = req.url!
1513
const parsed = new URL(url, 'http://vitejs.dev')
1614
const path = parsed.pathname || '/'
15+
const base = config.rawBase
1716

18-
if (path.startsWith(devBase)) {
17+
if (path.startsWith(base)) {
1918
// rewrite url to remove base. this ensures that other middleware does
2019
// not need to consider base being prepended or not
21-
req.url = url.replace(devBase, '/')
20+
req.url = stripBase(url, base)
2221
return next()
2322
}
2423

@@ -30,18 +29,19 @@ export function baseMiddleware({
3029
if (path === '/' || path === '/index.html') {
3130
// redirect root visit to based url with search and hash
3231
res.writeHead(302, {
33-
Location: config.base + (parsed.search || '') + (parsed.hash || '')
32+
Location: base + (parsed.search || '') + (parsed.hash || '')
3433
})
3534
res.end()
3635
return
3736
} else if (req.headers.accept?.includes('text/html')) {
3837
// non-based page visit
39-
const redirectPath = joinUrlSegments(config.base, url)
38+
const redirectPath =
39+
url + '/' !== base ? joinUrlSegments(base, url) : base
4040
res.writeHead(404, {
4141
'Content-Type': 'text/html'
4242
})
4343
res.end(
44-
`The server is configured with a public base URL of ${config.base} - ` +
44+
`The server is configured with a public base URL of ${base} - ` +
4545
`did you mean to visit <a href="${redirectPath}">${redirectPath}</a> instead?`
4646
)
4747
return

‎packages/vite/src/node/utils.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,8 @@ export async function resolveServerUrls(
868868
const hostname = await resolveHostname(options.host)
869869
const protocol = options.https ? 'https' : 'http'
870870
const port = address.port
871-
const base = config.base === './' || config.base === '' ? '/' : config.base
871+
const base =
872+
config.rawBase === './' || config.rawBase === '' ? '/' : config.rawBase
872873

873874
if (hostname.host && loopbackHosts.has(hostname.host)) {
874875
let hostnameName = hostname.name
@@ -1250,6 +1251,14 @@ export function joinUrlSegments(a: string, b: string): string {
12501251
return a + b
12511252
}
12521253

1254+
export function stripBase(path: string, base: string): string {
1255+
if (path === base) {
1256+
return '/'
1257+
}
1258+
const devBase = base.endsWith('/') ? base : base + '/'
1259+
return path.replace(RegExp('^' + devBase), '/')
1260+
}
1261+
12531262
export function arrayEqual(a: any[], b: any[]): boolean {
12541263
if (a === b) return true
12551264
if (a.length !== b.length) return false

‎playground/assets/__tests__/assets.spec.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import path from 'node:path'
12
import fetch from 'node-fetch'
23
import { describe, expect, test } from 'vitest'
34
import {
@@ -30,8 +31,12 @@ test('should have no 404s', () => {
3031
})
3132

3233
test('should get a 404 when using incorrect case', async () => {
33-
expect((await fetch(viteTestUrl + 'icon.png')).status).toBe(200)
34-
expect((await fetch(viteTestUrl + 'ICON.png')).status).toBe(404)
34+
expect((await fetch(path.posix.join(viteTestUrl, 'icon.png'))).status).toBe(
35+
200
36+
)
37+
expect((await fetch(path.posix.join(viteTestUrl, 'ICON.png'))).status).toBe(
38+
404
39+
)
3540
})
3641

3742
describe('injected scripts', () => {
@@ -312,7 +317,7 @@ test('new URL(`${dynamic}`, import.meta.url)', async () => {
312317

313318
test('new URL(`non-existent`, import.meta.url)', async () => {
314319
expect(await page.textContent('.non-existent-import-meta-url')).toMatch(
315-
'/foo/non-existent'
320+
new URL('non-existent', page.url()).pathname
316321
)
317322
})
318323

‎playground/assets/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
"private": true,
44
"version": "0.0.0",
55
"scripts": {
6+
"debug": "node --inspect-brk ../../packages/vite/bin/vite",
67
"dev": "vite",
78
"build": "vite build",
8-
"debug": "node --inspect-brk ../../packages/vite/bin/vite",
99
"preview": "vite preview",
1010
"dev:relative-base": "vite --config ./vite.config-relative-base.js dev",
1111
"build:relative-base": "vite --config ./vite.config-relative-base.js build",

‎playground/assets/vite.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const path = require('node:path')
44
* @type {import('vite').UserConfig}
55
*/
66
module.exports = {
7-
base: '/foo/',
7+
base: '/foo',
88
publicDir: 'static',
99
resolve: {
1010
alias: {

0 commit comments

Comments
 (0)
Please sign in to comment.