Skip to content

Commit 809d4bd

Browse files
authoredJan 22, 2021
feat: support base option during dev, deprecate build.base (#1556)
1 parent 6e7f652 commit 809d4bd

25 files changed

+384
-153
lines changed
 

‎docs/config/index.md

+7-7
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ export default ({ command, mode }) => {
9898

9999
See [Project Root](/guide/#project-root) for more details.
100100

101+
### base
102+
103+
- **Type:** `string`
104+
- **Default:** `/`
105+
106+
Base public path when served in development or production. Note the path should start and end with `/`. See [Public Base Path](/guide/build#public-base-path) for more details.
107+
101108
### mode
102109

103110
- **Type:** `string`
@@ -322,13 +329,6 @@ export default ({ command, mode }) => {
322329

323330
## Build Options
324331

325-
### build.base
326-
327-
- **Type:** `string`
328-
- **Default:** `/`
329-
330-
Base public path when served in production. Note the path should start and end with `/`. See [Public Base Path](/guide/build#public-base-path) for more details.
331-
332332
### build.target
333333

334334
- **Type:** `string`

‎docs/guide/build.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Legacy browsers can be supported via [@vitejs/plugin-legacy](https://github.com/
2323

2424
- Related: [Asset Handling](./features#asset-handling)
2525

26-
If you are deploying your project under a nested public path, simply specify the [`build.base` config option](/config/#build-base) and all asset paths will be rewritten accordingly. This option can also be specified as a command line flag, e.g. `vite build --base=/my/public/path/`.
26+
If you are deploying your project under a nested public path, simply specify the [`base` config option](/config/#base) and all asset paths will be rewritten accordingly. This option can also be specified as a command line flag, e.g. `vite build --base=/my/public/path/`.
2727

2828
JS-imported asset URLs, CSS `url()` references, and asset references in your `.html` files are all automatically adjusted to respect this option during build.
2929

‎docs/guide/env-and-mode.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Vite exposes env variables on the special **`import.meta.env`** object. Some bui
66

77
- **`import.meta.env.MODE`**: {string} the [mode](#modes) the app is running in.
88

9-
- **`import.meta.env.BASE_URL`**: {string} the base url the app is being served from. In development, this is always `/`. In production, this is determined by the [`build.base` config option](/config/#build-base).
9+
- **`import.meta.env.BASE_URL`**: {string} the base url the app is being served from. This is determined by the [`base` config option](/config/#base).
1010

1111
- **`import.meta.env.PROD`**: {boolean} whether the app is running in production.
1212

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

+27-3
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,40 @@ import {
1010

1111
const assetMatch = isBuild
1212
? /\/foo\/assets\/asset\.\w{8}\.png/
13-
: '/nested/asset.png'
13+
: '/foo/nested/asset.png'
1414

15-
const iconMatch = isBuild ? `/foo/icon.png` : `icon.png`
15+
const iconMatch = `/foo/icon.png`
1616

1717
test('should have no 404s', () => {
1818
browserLogs.forEach((msg) => {
1919
expect(msg).not.toMatch('404')
2020
})
2121
})
2222

23+
describe('injected scripts', () => {
24+
test('@vite/client', async () => {
25+
const hasClient = await page.$(
26+
'script[type="module"][src="/foo/@vite/client"]'
27+
)
28+
if (isBuild) {
29+
expect(hasClient).toBeFalsy()
30+
} else {
31+
expect(hasClient).toBeTruthy()
32+
}
33+
})
34+
35+
test('html-proxy', async () => {
36+
const hasHtmlProxy = await page.$(
37+
'script[type="module"][src="/foo/index.html?html-proxy&index=0.js"]'
38+
)
39+
if (isBuild) {
40+
expect(hasHtmlProxy).toBeFalsy()
41+
} else {
42+
expect(hasHtmlProxy).toBeTruthy()
43+
}
44+
})
45+
})
46+
2347
describe('raw references from /public', () => {
2448
test('load raw js from /public', async () => {
2549
expect(await page.textContent('.raw-js')).toMatch('[success]')
@@ -70,7 +94,7 @@ describe('css url() references', () => {
7094
})
7195

7296
test('base64 inline', async () => {
73-
const match = isBuild ? `data:image/png;base64` : `/icon.png`
97+
const match = isBuild ? `data:image/png;base64` : `/foo/nested/icon.png`
7498
expect(await getBg('.css-url-base64-inline')).toMatch(match)
7599
expect(await getBg('.css-url-quotes-base64-inline')).toMatch(match)
76100
})

‎packages/playground/assets/vite.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
* @type {import('vite').UserConfig}
33
*/
44
module.exports = {
5+
base: '/foo/',
56
build: {
6-
base: '/foo/',
77
outDir: 'dist/foo'
88
}
99
}

‎packages/plugin-legacy/index.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ function viteLegacyPlugin(options = {}) {
265265
tag: 'script',
266266
attrs: {
267267
type: 'module',
268-
src: `${config.build.base}${modernPolyfillFilename}`
268+
src: `${config.base}${modernPolyfillFilename}`
Has a conversation. Original line has a conversation.
269269
}
270270
})
271271
} else if (modernPolyfills.size) {
@@ -295,7 +295,7 @@ function viteLegacyPlugin(options = {}) {
295295
tag: 'script',
296296
attrs: {
297297
nomodule: true,
298-
src: `${config.build.base}${legacyPolyfillFilename}`
298+
src: `${config.base}${legacyPolyfillFilename}`
299299
},
300300
injectTo: 'body'
301301
})
@@ -318,7 +318,7 @@ function viteLegacyPlugin(options = {}) {
318318
// script content will stay consistent - which allows using a constant
319319
// hash value for CSP.
320320
id: legacyEntryId,
321-
'data-src': config.build.base + legacyEntryFilename
321+
'data-src': config.base + legacyEntryFilename
322322
},
323323
children: systemJSInlineCode,
324324
injectTo: 'body'

‎packages/plugin-vue/src/template.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ export function resolveTemplateCompilerOptions(
108108
// request
109109
if (filename.startsWith(options.root)) {
110110
assetUrlOptions = {
111-
base: '/' + slash(path.relative(options.root, path.dirname(filename)))
111+
base:
112+
options.devServer.config.base +
113+
slash(path.relative(options.root, path.dirname(filename)))
112114
}
113115
}
114116
} else {

‎packages/vite/src/client/client.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ErrorOverlay, overlayId } from './overlay'
33

44
// injected by the hmr plugin when served
55
declare const __ROOT__: string
6+
declare const __BASE__: string
67
declare const __MODE__: string
78
declare const __DEFINES__: Record<string, any>
89
declare const __HMR_PROTOCOL__: string
@@ -38,6 +39,8 @@ const socketProtocol =
3839
__HMR_PROTOCOL__ || (location.protocol === 'https:' ? 'wss' : 'ws')
3940
const socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`
4041
const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr')
42+
const base = __BASE__ || '/'
43+
const baseNoSlash = base.replace(/\/$/, '')
4144

4245
function warnFailedFetch(err: Error, path: string | string[]) {
4346
if (!err.message.match('fetch')) {
@@ -107,9 +110,10 @@ async function handleMessage(payload: HMRPayload) {
107110
// if html file is edited, only reload the page if the browser is
108111
// currently on that page.
109112
const pagePath = location.pathname
113+
const payloadPath = baseNoSlash + payload.path
110114
if (
111-
pagePath === payload.path ||
112-
(pagePath.endsWith('/') && pagePath + 'index.html' === payload.path)
115+
pagePath === payloadPath ||
116+
(pagePath.endsWith('/') && pagePath + 'index.html' === payloadPath)
113117
) {
114118
location.reload()
115119
}
@@ -259,6 +263,9 @@ export function removeStyle(id: string) {
259263
}
260264

261265
async function fetchUpdate({ path, acceptedPath, timestamp }: Update) {
266+
path = baseNoSlash + path
267+
acceptedPath = baseNoSlash + acceptedPath
268+
262269
const mod = hotModulesMap.get(path)
263270
if (!mod) {
264271
// In a code-splitting project,

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

+5-9
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { ssrManifestPlugin } from './ssr/ssrManifestPlugin'
3434
export interface BuildOptions {
3535
/**
3636
* Base public path when served in production.
37-
* @default '/'
37+
* @deprecated `base` is now a root-level config option.
3838
*/
3939
base?: string
4040
/**
@@ -168,11 +168,10 @@ export interface LibraryOptions {
168168

169169
export type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife'
170170

171-
export function resolveBuildOptions(
172-
raw?: BuildOptions
173-
): Required<BuildOptions> {
174-
const resolved: Required<BuildOptions> = {
175-
base: '/',
171+
export type ResolvedBuildOptions = Required<Omit<BuildOptions, 'base'>>
172+
173+
export function resolveBuildOptions(raw?: BuildOptions): ResolvedBuildOptions {
174+
const resolved: ResolvedBuildOptions = {
176175
target: 'modules',
177176
polyfillDynamicImport: raw?.target !== 'esnext' && !raw?.lib,
178177
outDir: 'dist',
@@ -207,9 +206,6 @@ export function resolveBuildOptions(
207206
resolved.target = 'es2019'
208207
}
209208

210-
// ensure base ending slash
211-
resolved.base = resolved.base.replace(/([^/])$/, '$1/')
212-
213209
// normalize false string into actual false
214210
if ((resolved.minify as any) === 'false') {
215211
resolved.minify = false

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface GlobalCLIOptions {
1717
config?: string
1818
c?: boolean | string
1919
root?: string
20+
base?: string
2021
r?: string
2122
mode?: string
2223
m?: string
@@ -38,6 +39,7 @@ function cleanOptions(options: GlobalCLIOptions) {
3839
delete ret.config
3940
delete ret.c
4041
delete ret.root
42+
delete ret.base
4143
delete ret.r
4244
delete ret.mode
4345
delete ret.m
@@ -50,6 +52,7 @@ function cleanOptions(options: GlobalCLIOptions) {
5052
cli
5153
.option('-c, --config <file>', `[string] use specified config file`)
5254
.option('-r, --root <path>', `[string] use specified root directory`)
55+
.option('--base <path>', `[string] public base path (default: /)`)
5356
.option('-l, --logLevel <level>', `[string] silent | error | warn | all`)
5457
.option('--clearScreen', `[boolean] allow/disable clear screen when logging`)
5558
.option('-d, --debug [feat]', `[string | boolean] show debug logs`)
@@ -77,6 +80,7 @@ cli
7780
try {
7881
const server = await createServer({
7982
root,
83+
base: options.base,
8084
mode: options.mode,
8185
configFile: options.config,
8286
logLevel: options.logLevel,
@@ -95,7 +99,6 @@ cli
9599
// build
96100
cli
97101
.command('build [root]')
98-
.option('--base <path>', `[string] public base path (default: /)`)
99102
.option('--target <target>', `[string] transpile target (default: 'modules')`)
100103
.option('--outDir <dir>', `[string] output directory (default: dist)`)
101104
.option(
@@ -141,6 +144,7 @@ cli
141144
try {
142145
await build({
143146
root,
147+
base: options.base,
144148
mode: options.mode,
145149
configFile: options.config,
146150
logLevel: options.logLevel,
@@ -169,6 +173,7 @@ cli
169173
const config = await resolveConfig(
170174
{
171175
root,
176+
base: options.base,
172177
configFile: options.config,
173178
logLevel: options.logLevel
174179
},

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

+61-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { resolvePlugin } from './plugins/resolve'
2222
import { createLogger, Logger, LogLevel } from './logger'
2323
import { DepOptimizationOptions } from './optimizer'
2424
import { createFilter } from '@rollup/pluginutils'
25+
import { ResolvedBuildOptions } from '.'
2526

2627
const debug = createDebugger('vite:config')
2728

@@ -112,6 +113,11 @@ export interface UserConfig {
112113
* Default: true
113114
*/
114115
clearScreen?: boolean
116+
/**
117+
* Base public path when served in development or production.
118+
* @default '/'
119+
*/
120+
base?: string
115121
}
116122

117123
export interface SSROptions {
@@ -136,9 +142,10 @@ export type ResolvedConfig = Readonly<
136142
alias: Alias[]
137143
plugins: readonly Plugin[]
138144
server: ServerOptions
139-
build: Required<BuildOptions>
145+
build: ResolvedBuildOptions
140146
assetsInclude: (file: string) => boolean
141147
logger: Logger
148+
base: string
142149
}
143150
>
144151

@@ -149,6 +156,7 @@ export async function resolveConfig(
149156
): Promise<ResolvedConfig> {
150157
let config = inlineConfig
151158
let mode = inlineConfig.mode || defaultMode
159+
const logger = createLogger(config.logLevel, config.clearScreen)
152160

153161
// some dependencies e.g. @vue/compiler-* relies on NODE_ENV for getting
154162
// production-specific behavior, so set it here even though we haven't
@@ -218,8 +226,45 @@ export async function resolveConfig(
218226
process.env.NODE_ENV = 'production'
219227
}
220228

229+
// resolve public base url
230+
// TODO remove when out of beta
231+
if (config.build?.base) {
232+
logger.warn(
233+
chalk.yellow.bold(
234+
`(!) "build.base" config option is deprecated. ` +
235+
`"base" is now a root-level config option.`
236+
)
237+
)
238+
config.base = config.build.base
239+
}
240+
241+
let BASE_URL = config.base || '/'
242+
if (!BASE_URL.startsWith('/') || !BASE_URL.endsWith('/')) {
243+
logger.warn(
244+
chalk.bold.yellow(
245+
`(!) "base" config option should start and end with "/".`
246+
)
247+
)
248+
if (!BASE_URL.startsWith('/')) BASE_URL = '/' + BASE_URL
249+
if (!BASE_URL.endsWith('/')) BASE_URL = BASE_URL + '/'
250+
}
251+
221252
const resolvedBuildOptions = resolveBuildOptions(config.build)
222253

254+
// TODO remove when out of beta
255+
Object.defineProperty(resolvedBuildOptions, 'base', {
256+
get() {
257+
logger.warn(
258+
chalk.yellow.bold(
259+
`(!) "build.base" config option is deprecated. ` +
260+
`"base" is now a root-level config option.\n` +
261+
new Error().stack
262+
)
263+
)
264+
return config.base
265+
}
266+
})
267+
223268
// resolve optimizer cache directory
224269
const pkgPath = lookupFile(
225270
resolvedRoot,
@@ -233,6 +278,17 @@ export async function resolveConfig(
233278
? createFilter(config.assetsInclude)
234279
: () => false
235280

281+
let hmr = config.server?.hmr === true ? {} : config.server?.hmr
282+
hmr = {
283+
...hmr,
284+
path: BASE_URL !== '/' ? BASE_URL.substr(1) : undefined
285+
}
286+
287+
const server = {
288+
...config.server,
289+
hmr
290+
}
291+
236292
const resolved = {
237293
...config,
238294
configFile: configFile ? normalizePath(configFile) : undefined,
@@ -244,19 +300,20 @@ export async function resolveConfig(
244300
optimizeCacheDir,
245301
alias: resolvedAlias,
246302
plugins: userPlugins,
247-
server: config.server || {},
303+
server,
248304
build: resolvedBuildOptions,
249305
env: {
250306
...userEnv,
251-
BASE_URL: command === 'build' ? resolvedBuildOptions.base : '/',
307+
BASE_URL,
252308
MODE: mode,
253309
DEV: !isProduction,
254310
PROD: isProduction
255311
},
256312
assetsInclude(file: string) {
257313
return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
258314
},
259-
logger: createLogger(config.logLevel, config.clearScreen)
315+
logger,
316+
base: BASE_URL
260317
}
261318

262319
resolved.plugins = await resolvePlugins(

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface Logger {
1010
warn(msg: string, options?: LogOptions): void
1111
error(msg: string, options?: LogOptions): void
1212
clearScreen(type: LogType): void
13+
hasWarned: boolean
1314
}
1415

1516
export interface LogOptions {
@@ -74,11 +75,13 @@ export function createLogger(
7475
}
7576
}
7677

77-
return {
78+
const logger: Logger = {
79+
hasWarned: false,
7880
info(msg, opts) {
7981
output('info', msg, opts)
8082
},
8183
warn(msg, opts) {
84+
logger.hasWarned = true
8285
output('warn', msg, opts)
8386
},
8487
error(msg, opts) {
@@ -90,4 +93,6 @@ export function createLogger(
9093
}
9194
}
9295
}
96+
97+
return logger
9398
}

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

+15-11
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin {
5959
s = s || (s = new MagicString(code))
6060
const [full, fileHandle, postfix = ''] = match
6161
const outputFilepath =
62-
config.build.base + this.getFileName(fileHandle) + postfix
62+
config.base + this.getFileName(fileHandle) + postfix
6363
s.overwrite(
6464
match.index,
6565
match.index + full.length,
@@ -118,18 +118,22 @@ export function fileToUrl(
118118
}
119119
}
120120

121-
function fileToDevUrl(id: string, { root }: ResolvedConfig) {
121+
function fileToDevUrl(id: string, { root, base }: ResolvedConfig) {
122+
let rtn: string
123+
122124
if (checkPublicFile(id, root)) {
123125
// in public dir, keep the url as-is
124-
return id
125-
}
126-
if (id.startsWith(root)) {
126+
rtn = id
127+
} else if (id.startsWith(root)) {
127128
// in project root, infer short public path
128-
return '/' + path.posix.relative(root, id)
129+
rtn = '/' + path.posix.relative(root, id)
130+
} else {
131+
// outside of project root, use absolute fs path
132+
// (this is special handled by the serve static middleware
133+
rtn = FS_PREFIX + id
129134
}
130-
// outside of project root, use absolute fs path
131-
// (this is special handled by the serve static middleware
132-
return FS_PREFIX + id
135+
136+
return path.posix.join(base, rtn)
133137
}
134138

135139
const assetCache = new WeakMap<ResolvedConfig, Map<string, string>>()
@@ -145,7 +149,7 @@ async function fileToBuiltUrl(
145149
skipPublicCheck = false
146150
): Promise<string> {
147151
if (!skipPublicCheck && checkPublicFile(id, config.root)) {
148-
return config.build.base + id.slice(1)
152+
return config.base + id.slice(1)
149153
}
150154

151155
let cache = assetCache.get(config)
@@ -197,7 +201,7 @@ export async function urlToBuiltUrl(
197201
pluginContext: PluginContext
198202
): Promise<string> {
199203
if (checkPublicFile(url, config.root)) {
200-
return config.build.base + url.slice(1)
204+
return config.base + url.slice(1)
201205
}
202206
const file = url.startsWith('/')
203207
? path.join(config.root, url)

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

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin {
3737

3838
return code
3939
.replace(`__MODE__`, JSON.stringify(config.mode))
40+
.replace(`__BASE__`, JSON.stringify(config.base))
4041
.replace(`__ROOT__`, JSON.stringify(config.root))
4142
.replace(`__DEFINES__`, JSON.stringify(config.define || {}))
4243
.replace(`__HMR_PROTOCOL__`, JSON.stringify(protocol))

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

+14-9
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,20 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
100100

101101
const urlReplacer: CssUrlReplacer = server
102102
? (url, importer) => {
103-
if (url.startsWith('/')) return url
104-
const filePath = normalizePath(
105-
path.resolve(path.dirname(importer || id), url)
106-
)
107-
if (filePath.startsWith(config.root)) {
108-
return filePath.slice(config.root.length)
103+
let rtn: string
104+
105+
if (url.startsWith('/')) {
106+
rtn = url
109107
} else {
110-
return `${FS_PREFIX}${filePath}`
108+
const filePath = normalizePath(
109+
path.resolve(path.dirname(importer || id), url)
110+
)
111+
rtn = filePath.startsWith(config.root)
112+
? filePath.slice(config.root.length)
113+
: `${FS_PREFIX}${filePath}`
111114
}
115+
116+
return path.posix.join(config.base, rtn)
112117
}
113118
: (url, importer) => {
114119
return urlToBuiltUrl(url, importer || id, config, this)
@@ -194,7 +199,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
194199
}
195200
return [
196201
`import { updateStyle, removeStyle } from ${JSON.stringify(
197-
CLIENT_PUBLIC_PATH
202+
path.posix.join(config.base, CLIENT_PUBLIC_PATH)
198203
)}`,
199204
`const id = ${JSON.stringify(id)}`,
200205
`const css = ${JSON.stringify(css)}`,
@@ -235,7 +240,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
235240

236241
// replace asset url references with resolved url
237242
chunkCSS = chunkCSS.replace(assetUrlRE, (_, fileId, postfix = '') => {
238-
return config.build.base + this.getFileName(fileId) + postfix
243+
return config.base + this.getFileName(fileId) + postfix
239244
})
240245

241246
if (config.build.cssCodeSplit) {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function dynamicImportPolyfillPlugin(config: ResolvedConfig): Plugin {
1111
const polyfillString =
1212
`const p = ${polyfill.toString()};` +
1313
`${isModernFlag}&&p(${JSON.stringify(
14-
path.posix.join(config.build.base, config.build.assetsDir, '/')
14+
path.posix.join(config.base, config.build.assetsDir, '/')
1515
)});`
1616

1717
return {

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

+80-57
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,20 @@ import MagicString from 'magic-string'
1010
import { checkPublicFile, assetUrlRE, urlToBuiltUrl } from './asset'
1111
import { isCSSRequest, chunkToEmittedCssFileMap } from './css'
1212
import { polyfillId } from './dynamicImportPolyfill'
13-
import { AttributeNode, NodeTransform, NodeTypes } from '@vue/compiler-dom'
13+
import {
14+
AttributeNode,
15+
NodeTransform,
16+
NodeTypes,
17+
ElementNode
18+
} from '@vue/compiler-dom'
1419

1520
const htmlProxyRE = /\?html-proxy&index=(\d+)\.js$/
1621
export const isHTMLProxy = (id: string) => htmlProxyRE.test(id)
17-
export const htmlCommentRE = /<!--[\s\S]*?-->/g
18-
export const scriptModuleRE = /(<script\b[^>]*type\s*=\s*(?:"module"|'module')[^>]*>)([\s\S]*?)<\/script>/gm
1922

20-
export function htmlPlugin(): Plugin {
23+
const htmlCommentRE = /<!--[\s\S]*?-->/g
24+
const scriptModuleRE = /(<script\b[^>]*type\s*=\s*(?:"module"|'module')[^>]*>)([\s\S]*?)<\/script>/gm
25+
26+
export function htmlInlineScriptProxyPlugin(): Plugin {
2127
return {
2228
name: 'vite:html',
2329

@@ -49,7 +55,7 @@ export function htmlPlugin(): Plugin {
4955
}
5056

5157
// this extends the config in @vue/compiler-sfc with <link href>
52-
const assetAttrsConfig: Record<string, string[]> = {
58+
export const assetAttrsConfig: Record<string, string[]> = {
5359
link: ['href'],
5460
video: ['src', 'poster'],
5561
source: ['src'],
@@ -58,6 +64,61 @@ const assetAttrsConfig: Record<string, string[]> = {
5864
use: ['xlink:href', 'href']
5965
}
6066

67+
export async function traverseHtml(
68+
html: string,
69+
filePath: string,
70+
visitor: NodeTransform
71+
) {
72+
// lazy load compiler
73+
const { parse, transform } = await import('@vue/compiler-dom')
74+
// @vue/compiler-core doesn't like lowercase doctypes
75+
html = html.replace(/<!doctype\s/i, '<!DOCTYPE ')
76+
try {
77+
const ast = parse(html, { comments: true })
78+
transform(ast, {
79+
nodeTransforms: [visitor]
80+
})
81+
} catch (e) {
82+
const parseError = {
83+
loc: filePath,
84+
frame: '',
85+
...formatParseError(e, filePath, html)
86+
}
87+
throw new Error(
88+
`Unable to parse ${JSON.stringify(parseError.loc)}\n${parseError.frame}`
89+
)
90+
}
91+
}
92+
93+
export function getScriptInfo(node: ElementNode) {
94+
let src: AttributeNode | undefined
95+
let isModule = false
96+
for (let i = 0; i < node.props.length; i++) {
97+
const p = node.props[i]
98+
if (p.type === NodeTypes.ATTRIBUTE) {
99+
if (p.name === 'src') {
100+
src = p
101+
} else if (p.name === 'type' && p.value && p.value.content === 'module') {
102+
isModule = true
103+
}
104+
}
105+
}
106+
return { src, isModule }
107+
}
108+
109+
function formatParseError(e: any, id: string, html: string): Error {
110+
// normalize the error to rollup format
111+
if (e.loc) {
112+
e.frame = generateCodeFrame(html, e.loc.start.offset)
113+
e.loc = {
114+
file: id,
115+
line: e.loc.start.line,
116+
column: e.loc.start.column
117+
}
118+
}
119+
return e
120+
}
121+
61122
/**
62123
* Compiles index.html into an entry js module
63124
*/
@@ -76,35 +137,12 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
76137
// pre-transform
77138
html = await applyHtmlTransforms(html, publicPath, id, preHooks)
78139

79-
function formatError(e: any): Error {
80-
// normalize the error to rollup format
81-
if (e.loc) {
82-
e.frame = generateCodeFrame(html, e.loc.start.offset)
83-
e.loc = {
84-
file: id,
85-
line: e.loc.start.line,
86-
column: e.loc.start.column
87-
}
88-
}
89-
return e
90-
}
91-
92-
// lazy load compiler-dom
93-
const { parse, transform } = await import('@vue/compiler-dom')
94-
// @vue/compiler-core doesn't like lowercase doctypes
95-
html = html.replace(/<!doctype\s/i, '<!DOCTYPE ')
96-
let ast
97-
try {
98-
ast = parse(html, { comments: true })
99-
} catch (e) {
100-
this.error(formatError(e))
101-
}
102-
103140
let js = ''
104141
const s = new MagicString(html)
105142
const assetUrls: AttributeNode[] = []
106143
let inlineModuleIndex = -1
107-
const viteHtmlTransform: NodeTransform = (node) => {
144+
145+
await traverseHtml(html, id, (node) => {
108146
if (node.type !== NodeTypes.ELEMENT) {
109147
return
110148
}
@@ -113,26 +151,19 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
113151

114152
// script tags
115153
if (node.tag === 'script') {
116-
const srcAttr = node.props.find(
117-
(p) => p.type === NodeTypes.ATTRIBUTE && p.name === 'src'
118-
) as AttributeNode
119-
const typeAttr = node.props.find(
120-
(p) => p.type === NodeTypes.ATTRIBUTE && p.name === 'type'
121-
) as AttributeNode
122-
const isJsModule =
123-
typeAttr && typeAttr.value && typeAttr.value.content === 'module'
124-
125-
const url = srcAttr && srcAttr.value && srcAttr.value.content
154+
const { src, isModule } = getScriptInfo(node)
155+
156+
const url = src && src.value && src.value.content
126157
if (url && checkPublicFile(url, config.root)) {
127158
// referencing public dir url, prefix with base
128159
s.overwrite(
129-
srcAttr.value!.loc.start.offset,
130-
srcAttr.value!.loc.end.offset,
131-
config.build.base + url.slice(1)
160+
src!.value!.loc.start.offset,
161+
src!.value!.loc.end.offset,
162+
config.base + url.slice(1)
132163
)
133164
}
134165

135-
if (isJsModule) {
166+
if (isModule) {
136167
inlineModuleIndex++
137168
if (url && !isExcludedUrl(url)) {
138169
// <script type="module" src="..."/>
@@ -170,7 +201,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
170201
s.overwrite(
171202
p.value.loc.start.offset,
172203
p.value.loc.end.offset,
173-
config.build.base + url.slice(1)
204+
config.base + url.slice(1)
174205
)
175206
}
176207
}
@@ -182,15 +213,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
182213
// ones in the end.
183214
s.remove(node.loc.start.offset, node.loc.end.offset)
184215
}
185-
}
186-
187-
try {
188-
transform(ast, {
189-
nodeTransforms: [viteHtmlTransform]
190-
})
191-
} catch (e) {
192-
this.error(formatError(e))
193-
}
216+
})
194217

195218
// for each encountered asset url, rewrite original html so that it
196219
// references the post-build location.
@@ -263,7 +286,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
263286
for (const [id, html] of processedHtml) {
264287
// resolve asset url references
265288
let result = html.replace(assetUrlRE, (_, fileId, postfix = '') => {
266-
return config.build.base + this.getFileName(fileId) + postfix
289+
return config.base + this.getFileName(fileId) + postfix
267290
})
268291

269292
// find corresponding entry chunk
@@ -335,7 +358,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
335358

336359
export interface HtmlTagDescriptor {
337360
tag: string
338-
attrs?: Record<string, string | boolean>
361+
attrs?: Record<string, string | boolean | undefined>
339362
children?: string | HtmlTagDescriptor[]
340363
/**
341364
* default: 'head-prepend'
@@ -466,7 +489,7 @@ export async function applyHtmlTransforms(
466489
}
467490

468491
function toPublicPath(filename: string, config: ResolvedConfig) {
469-
return isExternalUrl(filename) ? filename : config.build.base + filename
492+
return isExternalUrl(filename) ? filename : config.base + filename
470493
}
471494

472495
const headInjectRE = /<\/head>/

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

+13-5
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ function markExplicitImport(url: string) {
8585
* ```
8686
*/
8787
export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
88+
const clientPublicPath = path.posix.join(config.base, CLIENT_PUBLIC_PATH)
8889
let server: ViteDevServer
8990
let optimizedSource: string | undefined
9091

@@ -162,6 +163,10 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
162163
url: string,
163164
pos: number
164165
): Promise<[string, string]> => {
166+
if (config.base !== '/' && url.startsWith(config.base)) {
167+
url = url.replace(config.base, '/')
168+
}
169+
165170
const resolved = await this.resolve(url, importer)
166171

167172
if (!resolved) {
@@ -200,6 +205,9 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
200205
// mark non-js/css imports with `?import`
201206
url = markExplicitImport(url)
202207

208+
// prepend base path without trailing slash ( default empty string )
209+
url = path.posix.join(config.base, url)
210+
203211
// for relative js/css imports, inherit importer's version query
204212
// do not do this for unknown type imports, otherwise the appended
205213
// query can break 3rd party plugin's extension checks.
@@ -323,7 +331,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
323331
}
324332
}
325333
// skip client
326-
if (url === CLIENT_PUBLIC_PATH) {
334+
if (url === clientPublicPath) {
327335
continue
328336
}
329337

@@ -349,7 +357,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
349357
if (url !== rawUrl) {
350358
// for optimized cjs deps, support named imports by rewriting named
351359
// imports to const assignments.
352-
if (url.startsWith(OPTIMIZED_PREFIX)) {
360+
if (resolvedId.startsWith(OPTIMIZED_PREFIX)) {
353361
const depId = resolvedId.slice(OPTIMIZED_PREFIX.length)
354362
const optimizedId = makeLegalIdentifier(depId)
355363
optimizedSource =
@@ -433,16 +441,16 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
433441
)
434442
// inject hot context
435443
str().prepend(
436-
`import { createHotContext as __vite__createHotContext } from "${CLIENT_PUBLIC_PATH}";` +
444+
`import { createHotContext as __vite__createHotContext } from "${clientPublicPath}";` +
437445
`import.meta.hot = __vite__createHotContext(${JSON.stringify(
438-
importerModule.url
446+
path.posix.join(config.base, importerModule.url)
439447
)});`
440448
)
441449
}
442450

443451
if (needQueryInjectHelper) {
444452
str().prepend(
445-
`import { injectQuery as __vite__injectQuery } from "${CLIENT_PUBLIC_PATH}";`
453+
`import { injectQuery as __vite__injectQuery } from "${clientPublicPath}";`
446454
)
447455
}
448456

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,10 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
202202
if (filename === ownerFilename) return
203203
const chunk = bundle[filename] as OutputChunk | undefined
204204
if (chunk) {
205-
deps.add(config.build.base + chunk.fileName)
205+
deps.add(config.base + chunk.fileName)
206206
const cssId = chunkToEmittedCssFileMap.get(chunk)
207207
if (cssId) {
208-
deps.add(config.build.base + this.getFileName(cssId))
208+
deps.add(config.base + this.getFileName(cssId))
209209
}
210210
chunk.imports.forEach(addDeps)
211211
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { importAnalysisPlugin } from './importAnalysis'
88
import { cssPlugin, cssPostPlugin } from './css'
99
import { assetPlugin } from './asset'
1010
import { clientInjectionsPlugin } from './clientInjections'
11-
import { htmlPlugin } from './html'
11+
import { htmlInlineScriptProxyPlugin } from './html'
1212
import { wasmPlugin } from './wasm'
1313
import { webWorkerPlugin } from './worker'
1414
import { dynamicImportPolyfillPlugin } from './dynamicImportPolyfill'
@@ -42,7 +42,7 @@ export async function resolvePlugins(
4242
},
4343
config
4444
),
45-
htmlPlugin(),
45+
htmlInlineScriptProxyPlugin(),
4646
cssPlugin(config),
4747
config.esbuild !== false ? esbuildPlugin(config.esbuild) : null,
4848
jsonPlugin({

‎packages/vite/src/node/server/index.ts

+18-9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { FSWatcher, WatchOptions } from 'types/chokidar'
1818
import { resolveHttpsConfig } from '../server/https'
1919
import { createWebSocketServer, WebSocketServer } from '../server/ws'
20+
import { baseMiddleware } from './middlewares/base'
2021
import { proxyMiddleware, ProxyOptions } from './middlewares/proxy'
2122
import { transformMiddleware } from './middlewares/transform'
2223
import { indexHtmlMiddleware } from './middlewares/indexHtml'
@@ -109,6 +110,11 @@ export interface ServerOptions {
109110
* Create Vite dev server to be used as a middleware in an existing server
110111
*/
111112
middlewareMode?: boolean
113+
/**
114+
* Prepend this folder to http requests, for use when proxying vite as a subfolder
115+
* Should start and end with the `/` character
116+
*/
117+
base?: string
112118
}
113119

114120
/**
@@ -370,6 +376,11 @@ export async function createServer(
370376
middlewares.use(proxyMiddleware(server))
371377
}
372378

379+
// base
380+
if (config.base !== '/') {
381+
middlewares.use(baseMiddleware(server))
382+
}
383+
373384
// open in editor support
374385
middlewares.use('/__open-in-editor', launchEditorMiddleware())
375386

@@ -395,12 +406,7 @@ export async function createServer(
395406
{
396407
from: /\/$/,
397408
to({ parsedUrl }: any) {
398-
const rewritten = parsedUrl.pathname + 'index.html'
399-
if (fs.existsSync(path.join(root, rewritten))) {
400-
return rewritten
401-
} else {
402-
return `/index.html`
403-
}
409+
return parsedUrl.pathname + 'index.html'
404410
}
405411
}
406412
]
@@ -499,6 +505,7 @@ async function startServer(
499505
let hostname = options.host || 'localhost'
500506
const protocol = options.https ? 'https' : 'http'
501507
const info = server.config.logger.info
508+
const base = server.config.base
502509

503510
return new Promise((resolve, reject) => {
504511
const onError = (e: Error & { code?: string }) => {
@@ -521,7 +528,9 @@ async function startServer(
521528
httpServer.listen(port, () => {
522529
httpServer.removeListener('error', onError)
523530

524-
info(`\n Vite dev server running at:\n`, { clear: true })
531+
info(`\n Vite dev server running at:\n`, {
532+
clear: !server.config.logger.hasWarned
533+
})
525534
const interfaces = os.networkInterfaces()
526535
Object.keys(interfaces).forEach((key) =>
527536
(interfaces[key] || [])
@@ -535,7 +544,7 @@ async function startServer(
535544
}
536545
})
537546
.forEach(({ type, host }) => {
538-
const url = `${protocol}://${host}:${chalk.bold(port)}/`
547+
const url = `${protocol}://${host}:${chalk.bold(port)}${base}`
539548
info(` > ${type} ${chalk.cyan(url)}`)
540549
})
541550
)
@@ -570,7 +579,7 @@ async function startServer(
570579
}
571580

572581
if (options.open) {
573-
const path = typeof options.open === 'string' ? options.open : ''
582+
const path = typeof options.open === 'string' ? options.open : base
574583
openBrowser(
575584
`${protocol}://${hostname}:${port}${path}`,
576585
true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { parse as parseUrl } from 'url'
2+
import { ViteDevServer } from '..'
3+
import { Connect } from 'types/connect'
4+
5+
// this middleware is only active when (config.base !== '/')
6+
7+
export function baseMiddleware({
8+
config
9+
}: ViteDevServer): Connect.NextHandleFunction {
10+
const base = config.base
11+
12+
return (req, res, next) => {
13+
const url = req.url!
14+
const parsed = parseUrl(url)
15+
const path = parsed.pathname || '/'
16+
17+
if (path.startsWith(base)) {
18+
// rewrite url to remove base.. this ensures that other middleware does not need to consider base being prepended or not
19+
req.url = url.replace(base, '/')
20+
} else if (path === '/' || path === '/index.html') {
21+
// to prevent confusion, do not allow access at / if we have specified a base path
22+
res.statusCode = 404
23+
res.end()
24+
return
25+
}
26+
27+
next()
28+
}
29+
}

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

+74-19
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,99 @@
11
import fs from 'fs'
22
import path from 'path'
3+
import MagicString from 'magic-string'
4+
import { NodeTypes } from '@vue/compiler-dom'
35
import { Connect } from 'types/connect'
46
import { Plugin } from '../../plugin'
57
import {
6-
scriptModuleRE,
78
applyHtmlTransforms,
9+
getScriptInfo,
810
IndexHtmlTransformHook,
911
resolveHtmlTransforms,
10-
htmlCommentRE
12+
traverseHtml
1113
} from '../../plugins/html'
1214
import { ViteDevServer } from '../..'
1315
import { send } from '../send'
1416
import { CLIENT_PUBLIC_PATH, FS_PREFIX } from '../../constants'
1517
import { cleanUrl } from '../../utils'
18+
import { assetAttrsConfig } from '../../plugins/html'
1619

17-
const devHtmlHook: IndexHtmlTransformHook = (html, { path }) => {
18-
let index = -1
19-
const comments: string[] = []
20+
const devHtmlHook: IndexHtmlTransformHook = async (
21+
html,
22+
{ path: htmlPath, server }
23+
) => {
24+
const config = server?.config!
25+
const base = config.base || '/'
2026

21-
html = html
22-
.replace(htmlCommentRE, (m) => {
23-
comments.push(m)
24-
return `<!--VITE_COMMENT_${comments.length - 1}-->`
25-
})
26-
.replace(scriptModuleRE, (_match, _openTag, script) => {
27-
index++
28-
if (script) {
29-
// convert inline <script type="module"> into imported modules
30-
return `<script type="module" src="${path}?html-proxy&index=${index}.js"></script>`
27+
const s = new MagicString(html)
28+
let scriptModuleIndex = -1
29+
30+
await traverseHtml(html, htmlPath, (node) => {
31+
if (node.type !== NodeTypes.ELEMENT) {
32+
return
33+
}
34+
35+
// script tags
36+
if (node.tag === 'script') {
37+
const { src, isModule } = getScriptInfo(node)
38+
if (isModule) {
39+
scriptModuleIndex++
40+
}
41+
42+
if (src) {
43+
const url = src.value?.content || ''
44+
if (url.startsWith('/')) {
45+
// prefix with base
46+
s.overwrite(
47+
src.value!.loc.start.offset,
48+
src.value!.loc.end.offset,
49+
`"${config.base + url.slice(1)}"`
50+
)
51+
}
52+
} else if (isModule) {
53+
// inline js module. convert to src="proxy"
54+
s.overwrite(
55+
node.loc.start.offset,
56+
node.loc.end.offset,
57+
`<script type="module" src="${
58+
config.base + htmlPath.slice(1)
59+
}?html-proxy&index=${scriptModuleIndex}.js"></script>`
60+
)
3161
}
32-
return _match
33-
})
34-
.replace(/<!--VITE_COMMENT_(\d+)-->/g, (_, i) => comments[i])
62+
}
63+
64+
// elements with [href/src] attrs
65+
const assetAttrs = assetAttrsConfig[node.tag]
66+
if (assetAttrs) {
67+
for (const p of node.props) {
68+
if (
69+
p.type === NodeTypes.ATTRIBUTE &&
70+
p.value &&
71+
assetAttrs.includes(p.name)
72+
) {
73+
const url = p.value.content || ''
74+
if (url.startsWith('/')) {
75+
s.overwrite(
76+
p.value.loc.start.offset,
77+
p.value.loc.end.offset,
78+
`"${config.base + url.slice(1)}"`
79+
)
80+
}
81+
}
82+
}
83+
}
84+
})
85+
86+
html = s.toString()
3587

3688
return {
3789
html,
3890
tags: [
3991
{
4092
tag: 'script',
41-
attrs: { type: 'module', src: CLIENT_PUBLIC_PATH },
93+
attrs: {
94+
type: 'module',
95+
src: path.posix.join(base, CLIENT_PUBLIC_PATH)
96+
},
4297
injectTo: 'head-prepend'
4398
}
4499
]

‎packages/vite/src/node/ssr/ssrManifestPlugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { chunkToEmittedCssFileMap } from '../plugins/css'
55
export function ssrManifestPlugin(config: ResolvedConfig): Plugin {
66
// module id => preload assets mapping
77
const ssrManifest: Record<string, string[]> = {}
8-
const base = config.build.base
8+
const base = config.base
99

1010
return {
1111
name: 'vite:manifest',

‎scripts/jestPerTestSetup.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ beforeAll(async () => {
6666
if (!isBuildTest) {
6767
process.env.VITE_INLINE = 'inline-serve'
6868
server = await (await createServer(options)).listen()
69-
// use resolved port from server
70-
const url = ((global as any).viteTestUrl = `http://localhost:${server.config.server.port}`)
69+
// use resolved port/base from server
70+
const base = server.config.base === '/' ? '' : server.config.base
71+
const url = ((global as any).viteTestUrl = `http://localhost:${server.config.server.port}${base}`)
7172
await page.goto(url)
7273
} else {
7374
process.env.VITE_INLINE = 'inline-build'
@@ -100,7 +101,7 @@ function startStaticServer(): Promise<string> {
100101
try {
101102
config = require(configFile)
102103
} catch (e) {}
103-
const base = config?.build?.base || ''
104+
const base = (config?.base || '/') === '/' ? '' : config.base
104105

105106
// @ts-ignore
106107
if (config && config.__test__) {

0 commit comments

Comments
 (0)
Please sign in to comment.