From 809d4bd3bf62d3bc6b35f182178922d2ab2175f1 Mon Sep 17 00:00:00 2001 From: bompus Date: Fri, 22 Jan 2021 15:45:51 -0700 Subject: [PATCH] feat: support `base` option during dev, deprecate `build.base` (#1556) --- docs/config/index.md | 14 +- docs/guide/build.md | 2 +- docs/guide/env-and-mode.md | 2 +- .../assets/__tests__/assets.spec.ts | 30 +++- packages/playground/assets/vite.config.js | 2 +- packages/plugin-legacy/index.js | 6 +- packages/plugin-vue/src/template.ts | 4 +- packages/vite/src/client/client.ts | 11 +- packages/vite/src/node/build.ts | 14 +- packages/vite/src/node/cli.ts | 7 +- packages/vite/src/node/config.ts | 65 ++++++++- packages/vite/src/node/logger.ts | 7 +- packages/vite/src/node/plugins/asset.ts | 26 ++-- .../vite/src/node/plugins/clientInjections.ts | 1 + packages/vite/src/node/plugins/css.ts | 23 +-- .../src/node/plugins/dynamicImportPolyfill.ts | 2 +- packages/vite/src/node/plugins/html.ts | 137 ++++++++++-------- .../vite/src/node/plugins/importAnalysis.ts | 18 ++- .../src/node/plugins/importAnaysisBuild.ts | 4 +- packages/vite/src/node/plugins/index.ts | 4 +- packages/vite/src/node/server/index.ts | 27 ++-- .../vite/src/node/server/middlewares/base.ts | 29 ++++ .../src/node/server/middlewares/indexHtml.ts | 93 +++++++++--- .../vite/src/node/ssr/ssrManifestPlugin.ts | 2 +- scripts/jestPerTestSetup.ts | 7 +- 25 files changed, 384 insertions(+), 153 deletions(-) create mode 100644 packages/vite/src/node/server/middlewares/base.ts diff --git a/docs/config/index.md b/docs/config/index.md index 89ba6508cc1e15..c614f0298712ea 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -98,6 +98,13 @@ export default ({ command, mode }) => { See [Project Root](/guide/#project-root) for more details. +### base + +- **Type:** `string` +- **Default:** `/` + + 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. + ### mode - **Type:** `string` @@ -322,13 +329,6 @@ export default ({ command, mode }) => { ## Build Options -### build.base - -- **Type:** `string` -- **Default:** `/` - - 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. - ### build.target - **Type:** `string` diff --git a/docs/guide/build.md b/docs/guide/build.md index aefd258bf6b490..e3ac5e31be1b1e 100644 --- a/docs/guide/build.md +++ b/docs/guide/build.md @@ -23,7 +23,7 @@ Legacy browsers can be supported via [@vitejs/plugin-legacy](https://github.com/ - Related: [Asset Handling](./features#asset-handling) -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/`. +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/`. JS-imported asset URLs, CSS `url()` references, and asset references in your `.html` files are all automatically adjusted to respect this option during build. diff --git a/docs/guide/env-and-mode.md b/docs/guide/env-and-mode.md index b2d54d83f16900..9180e552cc0637 100644 --- a/docs/guide/env-and-mode.md +++ b/docs/guide/env-and-mode.md @@ -6,7 +6,7 @@ Vite exposes env variables on the special **`import.meta.env`** object. Some bui - **`import.meta.env.MODE`**: {string} the [mode](#modes) the app is running in. -- **`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). +- **`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). - **`import.meta.env.PROD`**: {boolean} whether the app is running in production. diff --git a/packages/playground/assets/__tests__/assets.spec.ts b/packages/playground/assets/__tests__/assets.spec.ts index eae3552fbb7101..0164d12d15b8ee 100644 --- a/packages/playground/assets/__tests__/assets.spec.ts +++ b/packages/playground/assets/__tests__/assets.spec.ts @@ -10,9 +10,9 @@ import { const assetMatch = isBuild ? /\/foo\/assets\/asset\.\w{8}\.png/ - : '/nested/asset.png' + : '/foo/nested/asset.png' -const iconMatch = isBuild ? `/foo/icon.png` : `icon.png` +const iconMatch = `/foo/icon.png` test('should have no 404s', () => { browserLogs.forEach((msg) => { @@ -20,6 +20,30 @@ test('should have no 404s', () => { }) }) +describe('injected scripts', () => { + test('@vite/client', async () => { + const hasClient = await page.$( + 'script[type="module"][src="/foo/@vite/client"]' + ) + if (isBuild) { + expect(hasClient).toBeFalsy() + } else { + expect(hasClient).toBeTruthy() + } + }) + + test('html-proxy', async () => { + const hasHtmlProxy = await page.$( + 'script[type="module"][src="/foo/index.html?html-proxy&index=0.js"]' + ) + if (isBuild) { + expect(hasHtmlProxy).toBeFalsy() + } else { + expect(hasHtmlProxy).toBeTruthy() + } + }) +}) + describe('raw references from /public', () => { test('load raw js from /public', async () => { expect(await page.textContent('.raw-js')).toMatch('[success]') @@ -70,7 +94,7 @@ describe('css url() references', () => { }) test('base64 inline', async () => { - const match = isBuild ? `data:image/png;base64` : `/icon.png` + const match = isBuild ? `data:image/png;base64` : `/foo/nested/icon.png` expect(await getBg('.css-url-base64-inline')).toMatch(match) expect(await getBg('.css-url-quotes-base64-inline')).toMatch(match) }) diff --git a/packages/playground/assets/vite.config.js b/packages/playground/assets/vite.config.js index 875c0cff93b254..9bfd9ac261a6c0 100644 --- a/packages/playground/assets/vite.config.js +++ b/packages/playground/assets/vite.config.js @@ -2,8 +2,8 @@ * @type {import('vite').UserConfig} */ module.exports = { + base: '/foo/', build: { - base: '/foo/', outDir: 'dist/foo' } } diff --git a/packages/plugin-legacy/index.js b/packages/plugin-legacy/index.js index 77f840cc811e99..4a3c90a47c4f2e 100644 --- a/packages/plugin-legacy/index.js +++ b/packages/plugin-legacy/index.js @@ -265,7 +265,7 @@ function viteLegacyPlugin(options = {}) { tag: 'script', attrs: { type: 'module', - src: `${config.build.base}${modernPolyfillFilename}` + src: `${config.base}${modernPolyfillFilename}` } }) } else if (modernPolyfills.size) { @@ -295,7 +295,7 @@ function viteLegacyPlugin(options = {}) { tag: 'script', attrs: { nomodule: true, - src: `${config.build.base}${legacyPolyfillFilename}` + src: `${config.base}${legacyPolyfillFilename}` }, injectTo: 'body' }) @@ -318,7 +318,7 @@ function viteLegacyPlugin(options = {}) { // script content will stay consistent - which allows using a constant // hash value for CSP. id: legacyEntryId, - 'data-src': config.build.base + legacyEntryFilename + 'data-src': config.base + legacyEntryFilename }, children: systemJSInlineCode, injectTo: 'body' diff --git a/packages/plugin-vue/src/template.ts b/packages/plugin-vue/src/template.ts index c7219284223649..0abe1dd0109c82 100644 --- a/packages/plugin-vue/src/template.ts +++ b/packages/plugin-vue/src/template.ts @@ -108,7 +108,9 @@ export function resolveTemplateCompilerOptions( // request if (filename.startsWith(options.root)) { assetUrlOptions = { - base: '/' + slash(path.relative(options.root, path.dirname(filename))) + base: + options.devServer.config.base + + slash(path.relative(options.root, path.dirname(filename))) } } } else { diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 949145a2103d0b..6af2ff8875792f 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -3,6 +3,7 @@ import { ErrorOverlay, overlayId } from './overlay' // injected by the hmr plugin when served declare const __ROOT__: string +declare const __BASE__: string declare const __MODE__: string declare const __DEFINES__: Record declare const __HMR_PROTOCOL__: string @@ -38,6 +39,8 @@ const socketProtocol = __HMR_PROTOCOL__ || (location.protocol === 'https:' ? 'wss' : 'ws') const socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}` const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr') +const base = __BASE__ || '/' +const baseNoSlash = base.replace(/\/$/, '') function warnFailedFetch(err: Error, path: string | string[]) { if (!err.message.match('fetch')) { @@ -107,9 +110,10 @@ async function handleMessage(payload: HMRPayload) { // if html file is edited, only reload the page if the browser is // currently on that page. const pagePath = location.pathname + const payloadPath = baseNoSlash + payload.path if ( - pagePath === payload.path || - (pagePath.endsWith('/') && pagePath + 'index.html' === payload.path) + pagePath === payloadPath || + (pagePath.endsWith('/') && pagePath + 'index.html' === payloadPath) ) { location.reload() } @@ -259,6 +263,9 @@ export function removeStyle(id: string) { } async function fetchUpdate({ path, acceptedPath, timestamp }: Update) { + path = baseNoSlash + path + acceptedPath = baseNoSlash + acceptedPath + const mod = hotModulesMap.get(path) if (!mod) { // In a code-splitting project, diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 5abf0146e9afef..a2856d1dbb57d0 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -34,7 +34,7 @@ import { ssrManifestPlugin } from './ssr/ssrManifestPlugin' export interface BuildOptions { /** * Base public path when served in production. - * @default '/' + * @deprecated `base` is now a root-level config option. */ base?: string /** @@ -168,11 +168,10 @@ export interface LibraryOptions { export type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife' -export function resolveBuildOptions( - raw?: BuildOptions -): Required { - const resolved: Required = { - base: '/', +export type ResolvedBuildOptions = Required> + +export function resolveBuildOptions(raw?: BuildOptions): ResolvedBuildOptions { + const resolved: ResolvedBuildOptions = { target: 'modules', polyfillDynamicImport: raw?.target !== 'esnext' && !raw?.lib, outDir: 'dist', @@ -207,9 +206,6 @@ export function resolveBuildOptions( resolved.target = 'es2019' } - // ensure base ending slash - resolved.base = resolved.base.replace(/([^/])$/, '$1/') - // normalize false string into actual false if ((resolved.minify as any) === 'false') { resolved.minify = false diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index ecf57946e791ae..09fe8a57cb0714 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -17,6 +17,7 @@ interface GlobalCLIOptions { config?: string c?: boolean | string root?: string + base?: string r?: string mode?: string m?: string @@ -38,6 +39,7 @@ function cleanOptions(options: GlobalCLIOptions) { delete ret.config delete ret.c delete ret.root + delete ret.base delete ret.r delete ret.mode delete ret.m @@ -50,6 +52,7 @@ function cleanOptions(options: GlobalCLIOptions) { cli .option('-c, --config ', `[string] use specified config file`) .option('-r, --root ', `[string] use specified root directory`) + .option('--base ', `[string] public base path (default: /)`) .option('-l, --logLevel ', `[string] silent | error | warn | all`) .option('--clearScreen', `[boolean] allow/disable clear screen when logging`) .option('-d, --debug [feat]', `[string | boolean] show debug logs`) @@ -77,6 +80,7 @@ cli try { const server = await createServer({ root, + base: options.base, mode: options.mode, configFile: options.config, logLevel: options.logLevel, @@ -95,7 +99,6 @@ cli // build cli .command('build [root]') - .option('--base ', `[string] public base path (default: /)`) .option('--target ', `[string] transpile target (default: 'modules')`) .option('--outDir ', `[string] output directory (default: dist)`) .option( @@ -141,6 +144,7 @@ cli try { await build({ root, + base: options.base, mode: options.mode, configFile: options.config, logLevel: options.logLevel, @@ -169,6 +173,7 @@ cli const config = await resolveConfig( { root, + base: options.base, configFile: options.config, logLevel: options.logLevel }, diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 396415e5223d7a..4111fc54341be5 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -22,6 +22,7 @@ import { resolvePlugin } from './plugins/resolve' import { createLogger, Logger, LogLevel } from './logger' import { DepOptimizationOptions } from './optimizer' import { createFilter } from '@rollup/pluginutils' +import { ResolvedBuildOptions } from '.' const debug = createDebugger('vite:config') @@ -112,6 +113,11 @@ export interface UserConfig { * Default: true */ clearScreen?: boolean + /** + * Base public path when served in development or production. + * @default '/' + */ + base?: string } export interface SSROptions { @@ -136,9 +142,10 @@ export type ResolvedConfig = Readonly< alias: Alias[] plugins: readonly Plugin[] server: ServerOptions - build: Required + build: ResolvedBuildOptions assetsInclude: (file: string) => boolean logger: Logger + base: string } > @@ -149,6 +156,7 @@ export async function resolveConfig( ): Promise { let config = inlineConfig let mode = inlineConfig.mode || defaultMode + const logger = createLogger(config.logLevel, config.clearScreen) // some dependencies e.g. @vue/compiler-* relies on NODE_ENV for getting // production-specific behavior, so set it here even though we haven't @@ -218,8 +226,45 @@ export async function resolveConfig( process.env.NODE_ENV = 'production' } + // resolve public base url + // TODO remove when out of beta + if (config.build?.base) { + logger.warn( + chalk.yellow.bold( + `(!) "build.base" config option is deprecated. ` + + `"base" is now a root-level config option.` + ) + ) + config.base = config.build.base + } + + let BASE_URL = config.base || '/' + if (!BASE_URL.startsWith('/') || !BASE_URL.endsWith('/')) { + logger.warn( + chalk.bold.yellow( + `(!) "base" config option should start and end with "/".` + ) + ) + if (!BASE_URL.startsWith('/')) BASE_URL = '/' + BASE_URL + if (!BASE_URL.endsWith('/')) BASE_URL = BASE_URL + '/' + } + const resolvedBuildOptions = resolveBuildOptions(config.build) + // TODO remove when out of beta + Object.defineProperty(resolvedBuildOptions, 'base', { + get() { + logger.warn( + chalk.yellow.bold( + `(!) "build.base" config option is deprecated. ` + + `"base" is now a root-level config option.\n` + + new Error().stack + ) + ) + return config.base + } + }) + // resolve optimizer cache directory const pkgPath = lookupFile( resolvedRoot, @@ -233,6 +278,17 @@ export async function resolveConfig( ? createFilter(config.assetsInclude) : () => false + let hmr = config.server?.hmr === true ? {} : config.server?.hmr + hmr = { + ...hmr, + path: BASE_URL !== '/' ? BASE_URL.substr(1) : undefined + } + + const server = { + ...config.server, + hmr + } + const resolved = { ...config, configFile: configFile ? normalizePath(configFile) : undefined, @@ -244,11 +300,11 @@ export async function resolveConfig( optimizeCacheDir, alias: resolvedAlias, plugins: userPlugins, - server: config.server || {}, + server, build: resolvedBuildOptions, env: { ...userEnv, - BASE_URL: command === 'build' ? resolvedBuildOptions.base : '/', + BASE_URL, MODE: mode, DEV: !isProduction, PROD: isProduction @@ -256,7 +312,8 @@ export async function resolveConfig( assetsInclude(file: string) { return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file) }, - logger: createLogger(config.logLevel, config.clearScreen) + logger, + base: BASE_URL } resolved.plugins = await resolvePlugins( diff --git a/packages/vite/src/node/logger.ts b/packages/vite/src/node/logger.ts index 5d8988dc8958d8..73cedc7cd78a9e 100644 --- a/packages/vite/src/node/logger.ts +++ b/packages/vite/src/node/logger.ts @@ -10,6 +10,7 @@ export interface Logger { warn(msg: string, options?: LogOptions): void error(msg: string, options?: LogOptions): void clearScreen(type: LogType): void + hasWarned: boolean } export interface LogOptions { @@ -74,11 +75,13 @@ export function createLogger( } } - return { + const logger: Logger = { + hasWarned: false, info(msg, opts) { output('info', msg, opts) }, warn(msg, opts) { + logger.hasWarned = true output('warn', msg, opts) }, error(msg, opts) { @@ -90,4 +93,6 @@ export function createLogger( } } } + + return logger } diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 62248025127253..0f86defc276b10 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -59,7 +59,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin { s = s || (s = new MagicString(code)) const [full, fileHandle, postfix = ''] = match const outputFilepath = - config.build.base + this.getFileName(fileHandle) + postfix + config.base + this.getFileName(fileHandle) + postfix s.overwrite( match.index, match.index + full.length, @@ -118,18 +118,22 @@ export function fileToUrl( } } -function fileToDevUrl(id: string, { root }: ResolvedConfig) { +function fileToDevUrl(id: string, { root, base }: ResolvedConfig) { + let rtn: string + if (checkPublicFile(id, root)) { // in public dir, keep the url as-is - return id - } - if (id.startsWith(root)) { + rtn = id + } else if (id.startsWith(root)) { // in project root, infer short public path - return '/' + path.posix.relative(root, id) + rtn = '/' + path.posix.relative(root, id) + } else { + // outside of project root, use absolute fs path + // (this is special handled by the serve static middleware + rtn = FS_PREFIX + id } - // outside of project root, use absolute fs path - // (this is special handled by the serve static middleware - return FS_PREFIX + id + + return path.posix.join(base, rtn) } const assetCache = new WeakMap>() @@ -145,7 +149,7 @@ async function fileToBuiltUrl( skipPublicCheck = false ): Promise { if (!skipPublicCheck && checkPublicFile(id, config.root)) { - return config.build.base + id.slice(1) + return config.base + id.slice(1) } let cache = assetCache.get(config) @@ -197,7 +201,7 @@ export async function urlToBuiltUrl( pluginContext: PluginContext ): Promise { if (checkPublicFile(url, config.root)) { - return config.build.base + url.slice(1) + return config.base + url.slice(1) } const file = url.startsWith('/') ? path.join(config.root, url) diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index d4913fe770d14c..ed21e81fb0b9cc 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -37,6 +37,7 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { return code .replace(`__MODE__`, JSON.stringify(config.mode)) + .replace(`__BASE__`, JSON.stringify(config.base)) .replace(`__ROOT__`, JSON.stringify(config.root)) .replace(`__DEFINES__`, JSON.stringify(config.define || {})) .replace(`__HMR_PROTOCOL__`, JSON.stringify(protocol)) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 8349749f2b255e..b17aa65b726677 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -100,15 +100,20 @@ export function cssPlugin(config: ResolvedConfig): Plugin { const urlReplacer: CssUrlReplacer = server ? (url, importer) => { - if (url.startsWith('/')) return url - const filePath = normalizePath( - path.resolve(path.dirname(importer || id), url) - ) - if (filePath.startsWith(config.root)) { - return filePath.slice(config.root.length) + let rtn: string + + if (url.startsWith('/')) { + rtn = url } else { - return `${FS_PREFIX}${filePath}` + const filePath = normalizePath( + path.resolve(path.dirname(importer || id), url) + ) + rtn = filePath.startsWith(config.root) + ? filePath.slice(config.root.length) + : `${FS_PREFIX}${filePath}` } + + return path.posix.join(config.base, rtn) } : (url, importer) => { return urlToBuiltUrl(url, importer || id, config, this) @@ -194,7 +199,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { } return [ `import { updateStyle, removeStyle } from ${JSON.stringify( - CLIENT_PUBLIC_PATH + path.posix.join(config.base, CLIENT_PUBLIC_PATH) )}`, `const id = ${JSON.stringify(id)}`, `const css = ${JSON.stringify(css)}`, @@ -235,7 +240,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { // replace asset url references with resolved url chunkCSS = chunkCSS.replace(assetUrlRE, (_, fileId, postfix = '') => { - return config.build.base + this.getFileName(fileId) + postfix + return config.base + this.getFileName(fileId) + postfix }) if (config.build.cssCodeSplit) { diff --git a/packages/vite/src/node/plugins/dynamicImportPolyfill.ts b/packages/vite/src/node/plugins/dynamicImportPolyfill.ts index 5ac75f35921288..b42b9db3b0c6e3 100644 --- a/packages/vite/src/node/plugins/dynamicImportPolyfill.ts +++ b/packages/vite/src/node/plugins/dynamicImportPolyfill.ts @@ -11,7 +11,7 @@ export function dynamicImportPolyfillPlugin(config: ResolvedConfig): Plugin { const polyfillString = `const p = ${polyfill.toString()};` + `${isModernFlag}&&p(${JSON.stringify( - path.posix.join(config.build.base, config.build.assetsDir, '/') + path.posix.join(config.base, config.build.assetsDir, '/') )});` return { diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index bfb6377fdc4991..874cb19804ff15 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -10,14 +10,20 @@ import MagicString from 'magic-string' import { checkPublicFile, assetUrlRE, urlToBuiltUrl } from './asset' import { isCSSRequest, chunkToEmittedCssFileMap } from './css' import { polyfillId } from './dynamicImportPolyfill' -import { AttributeNode, NodeTransform, NodeTypes } from '@vue/compiler-dom' +import { + AttributeNode, + NodeTransform, + NodeTypes, + ElementNode +} from '@vue/compiler-dom' const htmlProxyRE = /\?html-proxy&index=(\d+)\.js$/ export const isHTMLProxy = (id: string) => htmlProxyRE.test(id) -export const htmlCommentRE = //g -export const scriptModuleRE = /(]*type\s*=\s*(?:"module"|'module')[^>]*>)([\s\S]*?)<\/script>/gm -export function htmlPlugin(): Plugin { +const htmlCommentRE = //g +const scriptModuleRE = /(]*type\s*=\s*(?:"module"|'module')[^>]*>)([\s\S]*?)<\/script>/gm + +export function htmlInlineScriptProxyPlugin(): Plugin { return { name: 'vite:html', @@ -49,7 +55,7 @@ export function htmlPlugin(): Plugin { } // this extends the config in @vue/compiler-sfc with -const assetAttrsConfig: Record = { +export const assetAttrsConfig: Record = { link: ['href'], video: ['src', 'poster'], source: ['src'], @@ -58,6 +64,61 @@ const assetAttrsConfig: Record = { use: ['xlink:href', 'href'] } +export async function traverseHtml( + html: string, + filePath: string, + visitor: NodeTransform +) { + // lazy load compiler + const { parse, transform } = await import('@vue/compiler-dom') + // @vue/compiler-core doesn't like lowercase doctypes + html = html.replace(/ { + + await traverseHtml(html, id, (node) => { if (node.type !== NodeTypes.ELEMENT) { return } @@ -113,26 +151,19 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { // script tags if (node.tag === 'script') { - const srcAttr = node.props.find( - (p) => p.type === NodeTypes.ATTRIBUTE && p.name === 'src' - ) as AttributeNode - const typeAttr = node.props.find( - (p) => p.type === NodeTypes.ATTRIBUTE && p.name === 'type' - ) as AttributeNode - const isJsModule = - typeAttr && typeAttr.value && typeAttr.value.content === 'module' - - const url = srcAttr && srcAttr.value && srcAttr.value.content + const { src, isModule } = getScriptInfo(node) + + const url = src && src.value && src.value.content if (url && checkPublicFile(url, config.root)) { // referencing public dir url, prefix with base s.overwrite( - srcAttr.value!.loc.start.offset, - srcAttr.value!.loc.end.offset, - config.build.base + url.slice(1) + src!.value!.loc.start.offset, + src!.value!.loc.end.offset, + config.base + url.slice(1) ) } - if (isJsModule) { + if (isModule) { inlineModuleIndex++ if (url && !isExcludedUrl(url)) { // ` + const s = new MagicString(html) + let scriptModuleIndex = -1 + + await traverseHtml(html, htmlPath, (node) => { + if (node.type !== NodeTypes.ELEMENT) { + return + } + + // script tags + if (node.tag === 'script') { + const { src, isModule } = getScriptInfo(node) + if (isModule) { + scriptModuleIndex++ + } + + if (src) { + const url = src.value?.content || '' + if (url.startsWith('/')) { + // prefix with base + s.overwrite( + src.value!.loc.start.offset, + src.value!.loc.end.offset, + `"${config.base + url.slice(1)}"` + ) + } + } else if (isModule) { + // inline js module. convert to src="proxy" + s.overwrite( + node.loc.start.offset, + node.loc.end.offset, + `` + ) } - return _match - }) - .replace(//g, (_, i) => comments[i]) + } + + // elements with [href/src] attrs + const assetAttrs = assetAttrsConfig[node.tag] + if (assetAttrs) { + for (const p of node.props) { + if ( + p.type === NodeTypes.ATTRIBUTE && + p.value && + assetAttrs.includes(p.name) + ) { + const url = p.value.content || '' + if (url.startsWith('/')) { + s.overwrite( + p.value.loc.start.offset, + p.value.loc.end.offset, + `"${config.base + url.slice(1)}"` + ) + } + } + } + } + }) + + html = s.toString() return { html, tags: [ { tag: 'script', - attrs: { type: 'module', src: CLIENT_PUBLIC_PATH }, + attrs: { + type: 'module', + src: path.posix.join(base, CLIENT_PUBLIC_PATH) + }, injectTo: 'head-prepend' } ] diff --git a/packages/vite/src/node/ssr/ssrManifestPlugin.ts b/packages/vite/src/node/ssr/ssrManifestPlugin.ts index 2f724a790ebb92..0044e63df39b52 100644 --- a/packages/vite/src/node/ssr/ssrManifestPlugin.ts +++ b/packages/vite/src/node/ssr/ssrManifestPlugin.ts @@ -5,7 +5,7 @@ import { chunkToEmittedCssFileMap } from '../plugins/css' export function ssrManifestPlugin(config: ResolvedConfig): Plugin { // module id => preload assets mapping const ssrManifest: Record = {} - const base = config.build.base + const base = config.base return { name: 'vite:manifest', diff --git a/scripts/jestPerTestSetup.ts b/scripts/jestPerTestSetup.ts index dc7c6b28ca7bdd..b2cbfed0cbc51f 100644 --- a/scripts/jestPerTestSetup.ts +++ b/scripts/jestPerTestSetup.ts @@ -66,8 +66,9 @@ beforeAll(async () => { if (!isBuildTest) { process.env.VITE_INLINE = 'inline-serve' server = await (await createServer(options)).listen() - // use resolved port from server - const url = ((global as any).viteTestUrl = `http://localhost:${server.config.server.port}`) + // use resolved port/base from server + const base = server.config.base === '/' ? '' : server.config.base + const url = ((global as any).viteTestUrl = `http://localhost:${server.config.server.port}${base}`) await page.goto(url) } else { process.env.VITE_INLINE = 'inline-build' @@ -100,7 +101,7 @@ function startStaticServer(): Promise { try { config = require(configFile) } catch (e) {} - const base = config?.build?.base || '' + const base = (config?.base || '/') === '/' ? '' : config.base // @ts-ignore if (config && config.__test__) {