Skip to content

Commit

Permalink
feat: base without trailing slash (#10723)
Browse files Browse the repository at this point in the history
Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
Closes #9236
Closes #8770
Closes #8772
  • Loading branch information
BenediktAllendorf committed Nov 12, 2022
1 parent ce24c7c commit 8f87282
Show file tree
Hide file tree
Showing 11 changed files with 48 additions and 39 deletions.
2 changes: 1 addition & 1 deletion docs/config/server-options.md
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/build.ts
Expand Up @@ -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)
}
}

Expand Down
15 changes: 4 additions & 11 deletions packages/vite/src/node/config.ts
Expand Up @@ -323,6 +323,8 @@ export type ResolvedConfig = Readonly<
inlineConfig: InlineConfig
root: string
base: string
/** @internal */
rawBase: string
publicDir: string
cacheDir: string
command: 'build' | 'serve'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -834,10 +831,6 @@ export function resolveBaseUrl(
base = '/' + base
}
}
// ensure ending slash
if (!base.endsWith('/')) {
base += '/'
}

return base
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/asset.ts
Expand Up @@ -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(/^\//, ''))
Expand Down
8 changes: 5 additions & 3 deletions packages/vite/src/node/plugins/css.ts
Expand Up @@ -46,6 +46,7 @@ import {
processSrcSet,
removeDirectQuery,
requireResolveFromRootWithFallback,
stripBase,
stripBomTag
} from '../utils'
import type { Logger } from '../logger'
Expand Down Expand Up @@ -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
)
)
Expand Down
14 changes: 7 additions & 7 deletions packages/vite/src/node/plugins/importAnalysis.ts
Expand Up @@ -33,10 +33,12 @@ import {
isDataUrl,
isExternalUrl,
isJSRequest,
joinUrlSegments,
moduleListContains,
normalizePath,
prettifyUrl,
removeImportQuery,
stripBase,
stripBomTag,
timeFrom,
transformStableResult,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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) {
Expand Down
18 changes: 9 additions & 9 deletions 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()
}

Expand All @@ -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 <a href="${redirectPath}">${redirectPath}</a> instead?`
)
return
Expand Down
11 changes: 10 additions & 1 deletion packages/vite/src/node/utils.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions 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 {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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
)
})

Expand Down
2 changes: 1 addition & 1 deletion playground/assets/package.json
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion playground/assets/vite.config.js
Expand Up @@ -4,7 +4,7 @@ const path = require('node:path')
* @type {import('vite').UserConfig}
*/
module.exports = {
base: '/foo/',
base: '/foo',
publicDir: 'static',
resolve: {
alias: {
Expand Down

0 comments on commit 8f87282

Please sign in to comment.