diff --git a/docs/config/shared-options.md b/docs/config/shared-options.md index d6aa0017401a27..8879fbba41cf48 100644 --- a/docs/config/shared-options.md +++ b/docs/config/shared-options.md @@ -570,6 +570,54 @@ export default defineConfig({ }) ``` +## displayName + +- **Type:** `string` +- **Default:** `'vite'` + +The name shown in user-facing output in place of "vite". This covers the dev server and build banners, the default logger prefix (`[vite]`), and the messages the HMR client logs to the browser console. It is meant for tools built on top of Vite that want their own name in the output. + +Setting it also leaves the names of Vite's own plugins (such as `vite:css`) out of error messages, warnings and the error overlay; see [`hiddenPluginNames`](#hiddenpluginnames). It does not rename plugins, or change [`envPrefix`](#envprefix) or internal URLs such as `/@vite/client`. When using [`customLogger`](#customlogger), pass the same name to `createLogger(level, { displayName })` to keep the prefix consistent. + +```js +export default defineConfig({ + displayName: 'my-tool', +}) +``` + +## hiddenPluginNames + +- **Type:** `(string | RegExp)[]` +- **Default:** `[]` + +Plugin names to leave out of error messages, warnings and the error overlay. A string matches a name exactly; a RegExp is tested against it. When [`displayName`](#displayname) is set, the names of Vite's own plugins (`vite:*`) are left out as well. + +It is meant for tools built on top of Vite whose users neither added nor configure those plugins, so their names give the user nothing to act on. The plugins keep their names; only the output leaves them out. + +```js +export default defineConfig({ + displayName: 'my-tool', + hiddenPluginNames: [/^my-tool:/], +}) +``` + +## configHint + +- **Type:** `{ file: string; key?: string }` + +Where users set Vite's options, for the messages that tell them to change one: the error overlay's tip and the error for a blocked host. By default these name `vite.config.js`, or the overlay the config file that was loaded. + +`file` is the file users edit, and `key` the property Vite's options sit under in it, if any. It is meant for tools built on top of Vite that take Vite's options in their own config file: + +```js +export default defineConfig({ + displayName: 'my-tool', + // The overlay's tip says to set `vite.server.hmr.overlay` to `false` + // in `my-tool.config.js`. + configHint: { file: 'my-tool.config.js', key: 'vite' }, +}) +``` + ## clearScreen - **Type:** `boolean` diff --git a/packages/plugin-legacy/package.json b/packages/plugin-legacy/package.json index 85fb81f189f9a1..b60c9b174797b0 100644 --- a/packages/plugin-legacy/package.json +++ b/packages/plugin-legacy/package.json @@ -56,7 +56,7 @@ "picocolors": "^1.1.1", "tsdown": "^0.23.1", "unrun": "^0.3.1", - "vite": "workspace:*" + "vite": "workspace:@paul-browne/vite@*" }, "compatiblePackages": { "schemaVersion": 1, diff --git a/packages/vite/README.md b/packages/vite/README.md index 1460e75b9ce629..eebe7a35702221 100644 --- a/packages/vite/README.md +++ b/packages/vite/README.md @@ -1,3 +1,34 @@ +# @paul-browne/vite + +A fork of [Vite](https://github.com/vitejs/vite) with options for tools built on Vite that want their own name in its output ([vitejs/vite#23175](https://github.com/vitejs/vite/issues/23175), [vitejs/vite#23695](https://github.com/vitejs/vite/pull/23695)): + +- `displayName` shows the tool's name instead of "vite" in CLI banners, log prefixes and the browser console, and leaves Vite's own plugin names (`vite:*`) out of errors, warnings and the error overlay. +- `hiddenPluginNames` leaves more plugin names out the same way, such as the tool's own. +- `configHint` points the error overlay's tip and the blocked-host error at the tool's config file instead of `vite.config.js`. + +Each release is based on an upstream Vite release, with the patch number raised for the fork's own changes: 8.3.6 is upstream 8.3.4 plus `hiddenPluginNames`, `configHint`, and a fix that names the file when one of Rolldown's native plugins, such as the JSON plugin, fails to transform it. + +Install it under the `vite` name so plugins and `import ... from 'vite'` keep working: + +```sh +npm i -D vite@npm:@paul-browne/vite +``` + +```js +// vite.config.js +import { defineConfig } from 'vite' + +export default defineConfig({ + displayName: 'my-tool', + hiddenPluginNames: [/^my-tool:/], + configHint: { file: 'my-tool.config.js', key: 'vite' }, +}) +``` + +The original Vite README follows. + +--- + # Vite ⚡ > Next Generation Frontend Tooling diff --git a/packages/vite/package.json b/packages/vite/package.json index 8851c331465545..550c36625980b2 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,7 +1,7 @@ { - "name": "vite", - "version": "8.3.4", - "description": "Native-ESM powered web dev build tool", + "name": "@paul-browne/vite", + "version": "8.3.6", + "description": "Vite with a configurable displayName for whitelabelling (fork of vite)", "keywords": [ "build-tool", "dev-server", @@ -12,13 +12,13 @@ ], "homepage": "https://vite.dev", "bugs": { - "url": "https://github.com/vitejs/vite/issues" + "url": "https://github.com/Paul-Browne/vite/issues" }, "license": "MIT", "author": "Evan You", "repository": { "type": "git", - "url": "git+https://github.com/vitejs/vite.git", + "url": "git+https://github.com/Paul-Browne/vite.git", "directory": "packages/vite" }, "funding": "https://github.com/vitejs/vite?sponsor=1", @@ -133,6 +133,9 @@ "ufo": "^1.6.4", "ws": "^8.22.0" }, + "publishConfig": { + "access": "public" + }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1 || ^0.8.0", diff --git a/packages/vite/rewriteSelfImportsPlugin.ts b/packages/vite/rewriteSelfImportsPlugin.ts new file mode 100644 index 00000000000000..fbaab140435123 --- /dev/null +++ b/packages/vite/rewriteSelfImportsPlugin.ts @@ -0,0 +1,27 @@ +import type { Plugin } from 'rolldown' +import pkg from './package.json' with { type: 'json' } + +/** + * The bundles import `vite/module-runner` from this same package. When it is + * published under another name (e.g. `@paul-browne/vite`), point those imports + * at that name so they self-resolve instead of reaching an unrelated `vite`. + * Runs last so the type bundle's own checks still see `vite/module-runner`. + */ +export function rewriteSelfImportsPlugin(): Plugin { + return { + name: 'rewrite-self-imports', + generateBundle: { + order: 'post', + handler(_opts, bundle) { + if (pkg.name === 'vite') return + for (const chunk of Object.values(bundle)) { + if (chunk.type !== 'chunk') continue + chunk.code = chunk.code.replace( + /(["'])vite\/module-runner\1/g, + `$1${pkg.name}/module-runner$1`, + ) + } + }, + }, + } +} diff --git a/packages/vite/rolldown.config.ts b/packages/vite/rolldown.config.ts index 52a0f0526c60b9..7f6526f8bcec12 100644 --- a/packages/vite/rolldown.config.ts +++ b/packages/vite/rolldown.config.ts @@ -5,6 +5,7 @@ import MagicString from 'magic-string' import type { Plugin } from 'rolldown' import { defineConfig } from 'rolldown' import pkg from './package.json' with { type: 'json' } +import { rewriteSelfImportsPlugin } from './rewriteSelfImportsPlugin' import licensePlugin from './rollupLicensePlugin' // eslint-disable-next-line n/no-unsupported-features/node-builtins @@ -140,6 +141,7 @@ const nodeConfig = defineConfig({ writeTypesPlugin(), enableSourceMapsInWatchModePlugin(), externalizeDepsInWatchPlugin(), + rewriteSelfImportsPlugin(), ], }) diff --git a/packages/vite/rolldown.dts.config.ts b/packages/vite/rolldown.dts.config.ts index 4bc7f35636b47c..dcdd307116e37a 100644 --- a/packages/vite/rolldown.dts.config.ts +++ b/packages/vite/rolldown.dts.config.ts @@ -13,6 +13,7 @@ import { dts } from 'rolldown-plugin-dts' import { parseAst } from 'rolldown/parseAst' import type { ESTree } from 'rolldown/utils' import pkg from './package.json' with { type: 'json' } +import { rewriteSelfImportsPlugin } from './rewriteSelfImportsPlugin' type Directive = ESTree.Directive type ModuleExportName = ESTree.ModuleExportName @@ -59,6 +60,7 @@ export default defineConfig({ }, emitDtsOnly: true, }), + rewriteSelfImportsPlugin(), ], }) diff --git a/packages/vite/src/client/bundledDevClient.ts b/packages/vite/src/client/bundledDevClient.ts index d13287550d201e..edb1e155933951 100644 --- a/packages/vite/src/client/bundledDevClient.ts +++ b/packages/vite/src/client/bundledDevClient.ts @@ -10,6 +10,7 @@ import { registerBundledDevClient, transport, } from './client' +import { logPrefix } from './logPrefix' declare const __HMR_PARTIAL_ACCEPT__: boolean @@ -50,8 +51,8 @@ if (typeof DevRuntime !== 'undefined') { const bundledDevHmrClient = new BundledDevHMRClient( { - error: (err) => console.error('[vite]', err), - debug: (...msg) => console.debug('[vite]', ...msg), + error: (err) => console.error(logPrefix, err), + debug: (...msg) => console.debug(logPrefix, ...msg), }, transport, runtime, diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 1e7cfbeec6bd65..00c7eeab714875 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -9,6 +9,7 @@ import { } from '../shared/moduleRunnerTransport' import { wrapId } from '../shared/utils' import type { BundledDevHMRClient } from './bundledDevHmrClient' +import { logPrefix } from './logPrefix' import { ErrorOverlay, cspNonce, overlayId } from './overlay' // @ts-expect-error internal virtual module import '@vite/env' @@ -25,8 +26,7 @@ declare const __HMR_TIMEOUT__: number declare const __HMR_ENABLE_OVERLAY__: boolean declare const __WS_TOKEN__: string declare const __SERVER_FORWARD_CONSOLE__: any - -console.debug('[vite] connecting...') +console.debug(`${logPrefix} connecting...`) const importMetaUrl = new URL(import.meta.url) @@ -73,7 +73,7 @@ export const transport = normalizeModuleRunnerTransport( try { await wsTransport.connect(handlers) console.info( - '[vite] Direct websocket connection fallback. Check out https://vite.dev/config/server-options.html#server-hmr to remove the previous connection error.', + `${logPrefix} Direct websocket connection fallback. Check out https://vite.dev/config/server-options.html#server-hmr to remove the previous connection error.`, ) } catch (e) { if ( @@ -85,7 +85,7 @@ export const transport = normalizeModuleRunnerTransport( currentScriptHostURL.host + currentScriptHostURL.pathname.replace(/@vite\/client$/, '') console.error( - '[vite] failed to connect to websocket.\n' + + `${logPrefix} failed to connect to websocket.\n` + 'your current setup:\n' + ` (browser) ${currentScriptHost} <--[HTTP]--> ${serverHost} (server)\n` + ` (browser) ${socketHost} <--[WebSocket (failing)]--> ${directSocketHost} (server)\n` + @@ -95,7 +95,7 @@ export const transport = normalizeModuleRunnerTransport( } return } - console.error(`[vite] failed to connect to websocket (${e}). `) + console.error(`${logPrefix} failed to connect to websocket (${e}). `) throw e } }, @@ -146,8 +146,8 @@ function wrapIdIfNeeded(id: string): string { const hmrClient = new HMRClient( { - error: (err) => console.error('[vite]', err), - debug: (...msg) => console.debug('[vite]', ...msg), + error: (err) => console.error(logPrefix, err), + debug: (...msg) => console.debug(logPrefix, ...msg), }, transport, async function importUpdatedModule({ @@ -210,7 +210,7 @@ async function handleMessage(payload: HotPayload) { const activeHmrClient = bundledDevClient ?? hmrClient switch (payload.type) { case 'connected': - console.debug(`[vite] connected.`) + console.debug(`${logPrefix} connected.`) break case 'bundled-dev-update': bundledDevClient!.handlePush(payload) @@ -258,7 +258,7 @@ async function handleMessage(payload: HotPayload) { newLinkTag.href = new URL(newPath, el.href).href const removeOldEl = () => { el.remove() - console.debug(`[vite] css hot updated: ${searchUrl}`) + console.debug(`${logPrefix} css hot updated: ${searchUrl}`) resolve() } newLinkTag.addEventListener('load', removeOldEl) @@ -274,7 +274,9 @@ async function handleMessage(payload: HotPayload) { await activeHmrClient.notifyListeners(payload.event, payload.data) if (payload.event === 'vite:ws:disconnect') { if (hasDocument && !willUnload) { - console.log(`[vite] server connection lost. Polling for restart...`) + console.log( + `${logPrefix} server connection lost. Polling for restart...`, + ) const socket = payload.data.webSocket as WebSocket const url = new URL(socket.url) url.search = '' // remove query string including `token` @@ -325,7 +327,7 @@ async function handleMessage(payload: HotPayload) { createErrorOverlay(err) } else { console.error( - `[vite] Internal Server Error\n${err.message}\n${err.stack}`, + `${logPrefix} Internal Server Error\n${err.message}\n${err.stack}`, ) } } @@ -382,6 +384,8 @@ function waitForSuccessfulPing(socketUrl: string) { const blob = new Blob( [ '"use strict";', + // the functions below are stringified, so module-scope values they use must be redeclared + `const logPrefix = ${JSON.stringify(logPrefix)};`, `const waitForSuccessfulPingInternal = ${waitForSuccessfulPingInternal.toString()};`, `const fn = ${pingWorkerContentMain.toString()};`, `fn(${JSON.stringify(socketUrl)})`, @@ -439,17 +443,17 @@ function pingWorkerContentMain(socketUrl: string) { port.addEventListener('message', (event) => { const { visibility } = event.data visibilityManager.currentState = visibility - console.debug('[vite] new window visibility', visibility) + console.debug(`${logPrefix} new window visibility`, visibility) for (const listener of visibilityManager.listeners) { listener(visibility) } }) port.start() - console.debug('[vite] connected from window') + console.debug(`${logPrefix} connected from window`) waitForSuccessfulPingInternal(socketUrl, visibilityManager).then( () => { - console.debug('[vite] ping successful') + console.debug(`${logPrefix} ping successful`) try { port.postMessage({ type: 'success' }) } catch (error) { @@ -457,7 +461,7 @@ function pingWorkerContentMain(socketUrl: string) { } }, (error) => { - console.debug('[vite] error happened', error) + console.debug(`${logPrefix} error happened`, error) try { port.postMessage({ type: 'error', error }) } catch (error) { diff --git a/packages/vite/src/client/logPrefix.ts b/packages/vite/src/client/logPrefix.ts new file mode 100644 index 00000000000000..2f6c620984450f --- /dev/null +++ b/packages/vite/src/client/logPrefix.ts @@ -0,0 +1,4 @@ +// injected by the hmr plugin when served +declare const __DISPLAY_NAME__: string + +export const logPrefix = `[${__DISPLAY_NAME__}]` diff --git a/packages/vite/src/client/overlay.ts b/packages/vite/src/client/overlay.ts index a260035fd8b5eb..f5da9f6e42e825 100644 --- a/packages/vite/src/client/overlay.ts +++ b/packages/vite/src/client/overlay.ts @@ -3,8 +3,10 @@ import type { ErrorPayload } from '#types/hmrPayload' // injected by the hmr plugin when served declare const __BASE__: string declare const __HMR_CONFIG_NAME__: string +declare const __HMR_OVERLAY_OPTION__: string const hmrConfigName = __HMR_CONFIG_NAME__ +const hmrOverlayOption = __HMR_OVERLAY_OPTION__ const base = __BASE__ || '/' export const cspNonce = @@ -196,7 +198,7 @@ const createTemplate = () => ' key, or fix the code to dismiss.', h('br'), 'You can also disable this overlay by setting ', - h('code', { part: 'config-option-name' }, 'server.hmr.overlay'), + h('code', { part: 'config-option-name' }, hmrOverlayOption), ' to ', h('code', { part: 'config-option-value' }, 'false'), ' in ', diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index 3700ead59b26cd..0ee62a52cb03e5 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -1832,3 +1832,71 @@ function getOutputHashChanges( unchanged: names.filter((name) => map1[name] === map2[name]), } } + +test('build banner uses displayName', async () => { + const logger = createLogger('silent') + const infos: string[] = [] + logger.info = (msg) => { + infos.push(stripVTControlCharacters(msg)) + } + await build({ + root: resolve(dirname, 'packages/build-project'), + displayName: 'sitelo', + customLogger: logger, + build: { write: false }, + plugins: [ + { + name: 'test', + resolveId(id) { + if (id === 'entry.js') return '\0' + id + }, + load(id) { + if (id === '\0entry.js') return 'export default 1' + }, + }, + ], + input: 'entry.js', + }) + expect(infos[0]).toMatch(/^sitelo v\S+ building client environment/) +}) + +test("build warnings leave out hidden plugins' names", async () => { + const logger = createLogger('silent') + const warnings: string[] = [] + logger.warn = (msg) => { + warnings.push(stripVTControlCharacters(msg)) + } + const warningPlugin = (name: string) => ({ + name, + buildStart(this: { warn(message: string): void }) { + this.warn(`a warning from a plugin`) + }, + }) + await build({ + root: resolve(dirname, 'packages/build-project'), + displayName: 'sitelo', + hiddenPluginNames: ['my-tool-plugin'], + customLogger: logger, + build: { write: false }, + plugins: [ + warningPlugin('vite:stand-in'), + warningPlugin('my-tool-plugin'), + warningPlugin('someone-elses-plugin'), + { + name: 'test', + resolveId(id) { + if (id === 'entry.js') return '\0' + id + }, + load(id) { + if (id === '\0entry.js') return 'export default 1' + }, + }, + ], + input: 'entry.js', + }) + expect(warnings).toStrictEqual([ + 'a warning from a plugin', + 'a warning from a plugin', + '[plugin someone-elses-plugin] a warning from a plugin', + ]) +}) diff --git a/packages/vite/src/node/__tests__/config.spec.ts b/packages/vite/src/node/__tests__/config.spec.ts index f41ad578748eeb..b0047e1edca4c3 100644 --- a/packages/vite/src/node/__tests__/config.spec.ts +++ b/packages/vite/src/node/__tests__/config.spec.ts @@ -1294,6 +1294,17 @@ describe('resolveEnvPrefix', () => { consoleWarnSpy.mockRestore() }) + test('uses displayName in the whitespace warning', () => { + const consoleWarnSpy = vi + .spyOn(console, 'warn') + .mockImplementation(() => {}) + resolveEnvPrefix({ envPrefix: 'WITH SPACE', displayName: 'sitelo' }) + expect(stripVTControlCharacters(consoleWarnSpy.mock.calls[0][0])).toMatch( + /^\[sitelo\] Warning:/, + ) + consoleWarnSpy.mockRestore() + }) + test('should work correctly for valid envPrefix value', () => { const config: UserConfig = { envPrefix: ['CUSTOM_'] } expect(resolveEnvPrefix(config)).toMatchObject(['CUSTOM_']) @@ -2211,7 +2222,7 @@ describe('loadConfigFromFile', () => { ))! expect(config).toMatchInlineSnapshot(` { - "jsonValue": "vite", + "jsonValue": "@paul-browne/vite", } `) }) @@ -2466,3 +2477,47 @@ describe('resolveServerOptions', () => { } }) }) + +describe('displayName', () => { + test('defaults to vite', async () => { + const config = await resolveConfig({ configFile: false }, 'serve') + expect(config.displayName).toBe('vite') + }) + + test('can be set by the user', async () => { + const config = await resolveConfig( + { configFile: false, displayName: 'sitelo' }, + 'serve', + ) + expect(config.displayName).toBe('sitelo') + }) + + test("hides no plugin names unless it is set or they're listed", async () => { + const config = await resolveConfig({ configFile: false }, 'serve') + expect(config.hiddenPluginNames).toStrictEqual([]) + }) + + test("hides Vite's own plugin names when it is set", async () => { + const config = await resolveConfig( + { + configFile: false, + displayName: 'sitelo', + hiddenPluginNames: ['my-plugin'], + }, + 'serve', + ) + expect(config.hiddenPluginNames).toStrictEqual([ + /^vite:/, + /^builtin:vite-/, + 'my-plugin', + ]) + }) + + test('hides listed plugin names without it', async () => { + const config = await resolveConfig( + { configFile: false, hiddenPluginNames: [/^my-/] }, + 'serve', + ) + expect(config.hiddenPluginNames).toStrictEqual([/^my-/]) + }) +}) diff --git a/packages/vite/src/node/__tests__/logger.spec.ts b/packages/vite/src/node/__tests__/logger.spec.ts index 50c560c3b3548e..365fdb46332f8c 100644 --- a/packages/vite/src/node/__tests__/logger.spec.ts +++ b/packages/vite/src/node/__tests__/logger.spec.ts @@ -1,6 +1,6 @@ import { stripVTControlCharacters } from 'node:util' import { describe, expect, test } from 'vitest' -import { printServerUrls } from '../logger' +import { createLogger, printServerUrls } from '../logger' import type { ResolvedServerUrls } from '../server' function collectServerUrls(urls: ResolvedServerUrls): string { @@ -65,3 +65,35 @@ describe('printServerUrls', () => { expect(messages).toMatchSnapshot() }) }) + +describe('createLogger', () => { + function collectLogs(options: Parameters[1]) { + const messages: string[] = [] + const fakeConsole = { + log: (msg: string) => messages.push(stripVTControlCharacters(msg)), + } as Console + const logger = createLogger('info', { + allowClearScreen: false, + console: fakeConsole, + ...options, + }) + logger.info('hello', { timestamp: true }) + return messages + } + + test('uses [vite] as the default prefix', () => { + expect(collectLogs({})[0]).toMatch(/ \[vite\] hello$/) + }) + + test('derives the prefix from displayName', () => { + expect(collectLogs({ displayName: 'sitelo' })[0]).toMatch( + / \[sitelo\] hello$/, + ) + }) + + test('an explicit prefix takes precedence over displayName', () => { + expect( + collectLogs({ displayName: 'sitelo', prefix: '[custom]' })[0], + ).toMatch(/ \[custom\] hello$/) + }) +}) diff --git a/packages/vite/src/node/__tests__/plugins/clientInjections.spec.ts b/packages/vite/src/node/__tests__/plugins/clientInjections.spec.ts index b2a5e8a43228f2..1c7d766b2872db 100644 --- a/packages/vite/src/node/__tests__/plugins/clientInjections.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/clientInjections.spec.ts @@ -1,6 +1,8 @@ import path from 'node:path' import { expect, test } from 'vitest' +import type { InlineConfig } from '../../config' import { getRolldownDevRuntimeFiles } from '../../plugins/clientInjections' +import { createServer } from '../../server' // Catch a rolldown layout change here, instead of as a 404 in the browser. The runtime may // be one file with no imports, or an entry that imports helper files with relative paths. @@ -16,3 +18,54 @@ test('serves every file the rolldown dev runtime imports', () => { } } }) + +test('injects displayName into the HMR client', async () => { + const server = await createServer({ + configFile: false, + root: import.meta.dirname, + logLevel: 'silent', + displayName: 'sitelo', + optimizeDeps: { noDiscovery: true }, + server: { middlewareMode: true, ws: false }, + }) + try { + const result = + await server.environments.client.transformRequest('/@vite/client') + expect(result?.code).not.toContain('__DISPLAY_NAME__') + expect(result?.code).toContain('const logPrefix = `[${"sitelo"}]`') + } finally { + await server.close() + } +}) + +async function transformClient(config: InlineConfig) { + const server = await createServer({ + configFile: false, + root: import.meta.dirname, + logLevel: 'silent', + optimizeDeps: { noDiscovery: true }, + server: { middlewareMode: true, ws: false }, + ...config, + }) + try { + const result = + await server.environments.client.transformRequest('/@vite/client') + return result!.code + } finally { + await server.close() + } +} + +test('the overlay tip names vite.config.js by default', async () => { + const code = await transformClient({}) + expect(code).toContain('const hmrConfigName = "vite.config.js"') + expect(code).toContain('const hmrOverlayOption = "server.hmr.overlay"') +}) + +test('the overlay tip names the file and key in configHint', async () => { + const code = await transformClient({ + configHint: { file: 'my-tool.config.js', key: 'vite' }, + }) + expect(code).toContain('const hmrConfigName = "my-tool.config.js"') + expect(code).toContain('const hmrOverlayOption = "vite.server.hmr.overlay"') +}) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 9731985f663cd7..5c551b9fbae8b7 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -75,6 +75,7 @@ import { createDebugger, displayTime, getPkgName, + isPluginNameHidden, joinUrlSegments, mergeConfig, mergeWithDefaults, @@ -855,7 +856,7 @@ async function buildEnvironment( logger.info( colors.cyan( - `vite v${VERSION} ${colors.green( + `${config.displayName} v${VERSION} ${colors.green( `building ${environment.name} environment for ${environment.config.mode}...`, )}`, ), @@ -1149,7 +1150,7 @@ export function onRollupLog( // throw unless it's commonjs external... if (!id || !id.endsWith('?commonjs-external')) { throw new Error( - `[vite]: Rolldown failed to resolve import "${exporter}" from "${id}".\n` + + `[${environment.config.displayName}]: Rolldown failed to resolve import "${exporter}" from "${id}".\n` + `This is most likely unintended because it can break your application at runtime.\n` + `If you do want to externalize this module explicitly add it to\n` + `\`build.rolldownOptions.external\``, @@ -1174,7 +1175,10 @@ export function onRollupLog( // append plugin name to align with Rollup's behavior let message = logging.message - if (logging.plugin) { + if ( + logging.plugin && + !isPluginNameHidden(logging.plugin, environment.config.hiddenPluginNames) + ) { message = `[plugin ${logging.plugin}] ${message}` } diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index dd4e4d6681df14..4cf1db2995d689 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -266,7 +266,7 @@ cli info( `\n ${colors.green( - `${colors.bold('VITE')} v${VERSION}`, + `${colors.bold(server.config.displayName.toUpperCase())} v${VERSION}`, )}${modeString} ${startupDurationString}\n`, { clear: !hasExistingLogs, diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index cdf66df7f9588c..cd42b00283ae86 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -63,6 +63,7 @@ import { DEFAULT_SERVER_MAIN_FIELDS, ENV_ENTRY, FS_PREFIX, + VITE_PLUGIN_NAMES, } from './constants' import { loadEnv, resolveEnvPrefix } from './env' import { createIdResolver } from './idResolver' @@ -481,6 +482,38 @@ export interface UserConfig extends DefaultEnvironmentOptions { * Custom logger. */ customLogger?: Logger + /** + * Name shown in user-facing output instead of "vite": the startup and + * build banners, the default logger prefix, and the browser console + * messages of the HMR client. Useful for tools built on top of Vite. + * + * It also leaves the names of Vite's own plugins (`vite:*`) out of error + * messages, warnings and the error overlay; see `hiddenPluginNames`. It + * does not rename plugins, or change `envPrefix` or internal URLs such + * as `/@vite/client`. + * @default 'vite' + */ + displayName?: string + /** + * Plugin names to leave out of error messages, warnings and the error + * overlay. A string matches a name exactly; a RegExp is tested against + * it. When `displayName` is set, Vite's own plugins (`vite:*`) are left + * out as well. + * + * Useful for tools built on top of Vite, whose users neither added nor + * configure those plugins. The plugins keep their names. + */ + hiddenPluginNames?: (string | RegExp)[] + /** + * Where users set Vite's options, for the messages that tell them to + * change one: the error overlay's tip and the blocked-host error. By + * default these name `vite.config.js`, or the overlay the config file + * that was loaded. + * + * Useful for tools built on top of Vite that take Vite's options in + * their own config file. + */ + configHint?: ConfigHint /** * @default true */ @@ -556,6 +589,18 @@ export interface UserConfig extends DefaultEnvironmentOptions { devtools?: boolean | DevToolsConfig } +export interface ConfigHint { + /** + * The file users edit to set Vite's options, e.g. `my-tool.config.js`. + */ + file: string + /** + * The property Vite's options sit under in that file, if any: with + * `'vite'`, the overlay's tip says to set `vite.server.hmr.overlay`. + */ + key?: string +} + export interface HTMLOptions { /** * A nonce value placeholder that will be used when generating script/style tags. @@ -745,6 +790,13 @@ export interface ResolvedConfig extends Readonly< assetsInclude: (file: string) => boolean rawAssetsInclude: (string | RegExp)[] logger: Logger + displayName: string + /** + * `hiddenPluginNames`, with Vite's own plugins added when `displayName` + * is set. + */ + hiddenPluginNames: readonly (string | RegExp)[] + configHint: ConfigHint | undefined /** * Create an internal resolver to be used in special scenarios, e.g. * optimizer & handling css `@imports`. @@ -838,6 +890,7 @@ async function loadDevToolsIntegrationPlugins( const logger = createLogger(config.logLevel, { allowClearScreen: config.clearScreen, customLogger: config.customLogger, + displayName: config.displayName, }) logger.error( colors.red( @@ -926,6 +979,7 @@ const configDefaults = Object.freeze({ }, logLevel: 'info', customLogger: undefined, + displayName: 'vite', clearScreen: true, envDir: undefined, envPrefix: 'VITE_', @@ -1586,6 +1640,7 @@ export async function resolveConfig( const logger = createLogger(config.logLevel, { allowClearScreen: config.clearScreen, customLogger: config.customLogger, + displayName: config.displayName, }) if (devtoolsConfigChanged) { @@ -2107,6 +2162,15 @@ export async function resolveConfig( }, rawAssetsInclude: config.assetsInclude ? arraify(config.assetsInclude) : [], logger, + displayName: config.displayName ?? configDefaults.displayName, + hiddenPluginNames: [ + ...((config.displayName ?? configDefaults.displayName) !== + configDefaults.displayName + ? VITE_PLUGIN_NAMES + : []), + ...(config.hiddenPluginNames ?? []), + ], + configHint: config.configHint, packageCache, worker: resolvedWorkerOptions, appType: config.appType ?? 'spa', @@ -2821,6 +2885,7 @@ async function runConfigHook( const tempLogger = createLogger(config.logLevel, { allowClearScreen: config.clearScreen, customLogger: config.customLogger, + displayName: config.displayName, }) const context = new BasicMinimalPluginContext< Omit diff --git a/packages/vite/src/node/constants.ts b/packages/vite/src/node/constants.ts index 751deab5e64bcb..76aa1200828e0e 100644 --- a/packages/vite/src/node/constants.ts +++ b/packages/vite/src/node/constants.ts @@ -121,6 +121,11 @@ export const VITE_PACKAGE_DIR: string = resolve( '../../..', ) +/** + * Names of Vite's own plugins, left out of output when `displayName` is set. + */ +export const VITE_PLUGIN_NAMES: readonly RegExp[] = [/^vite:/, /^builtin:vite-/] + export const CLIENT_ENTRY: string = resolve( VITE_PACKAGE_DIR, 'dist/client/client.mjs', diff --git a/packages/vite/src/node/deprecations.ts b/packages/vite/src/node/deprecations.ts index f51b71c223e44c..44e2eb51decc7a 100644 --- a/packages/vite/src/node/deprecations.ts +++ b/packages/vite/src/node/deprecations.ts @@ -65,7 +65,7 @@ export function warnFutureDeprecation( ) return - let msg = `[vite future] ${deprecationMessages[type]}` + let msg = `[${config.displayName} future] ${deprecationMessages[type]}` if (extraMessage) { msg += ` ${extraMessage}` } diff --git a/packages/vite/src/node/env.ts b/packages/vite/src/node/env.ts index 3800560e02ff9e..2238d1b70dc7ca 100644 --- a/packages/vite/src/node/env.ts +++ b/packages/vite/src/node/env.ts @@ -108,6 +108,7 @@ export function loadEnv( export function resolveEnvPrefix({ envPrefix = 'VITE_', + displayName = 'vite', }: UserConfig): string[] { envPrefix = arraify(envPrefix) if (envPrefix.includes('')) { @@ -119,7 +120,7 @@ export function resolveEnvPrefix({ // eslint-disable-next-line no-console console.warn( colors.yellow( - `[vite] Warning: envPrefix option contains values with whitespace, which does not work in practice.`, + `[${displayName}] Warning: envPrefix option contains values with whitespace, which does not work in practice.`, ), ) } diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 5020bec7659a96..2c358ae46bd888 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -109,6 +109,7 @@ export { loadEnv, resolveEnvPrefix } from './env' export type { AppType, ConfigEnv, + ConfigHint, ExperimentalOptions, HTMLOptions, InlineConfig, diff --git a/packages/vite/src/node/logger.ts b/packages/vite/src/node/logger.ts index 3fcc2a923c1fa2..c258867a67bb0e 100644 --- a/packages/vite/src/node/logger.ts +++ b/packages/vite/src/node/logger.ts @@ -47,7 +47,16 @@ function clearScreen() { } export interface LoggerOptions { + /** + * Prefix shown before timestamped messages. + * @default `[${displayName}]` + */ prefix?: string + /** + * Name used for the default prefix. + * @default 'vite' + */ + displayName?: string allowClearScreen?: boolean customLogger?: Logger console?: Console @@ -75,7 +84,8 @@ export function createLogger( const loggedErrors = new WeakSet() const { - prefix = '[vite]', + displayName = 'vite', + prefix = `[${displayName}]`, allowClearScreen = true, console = globalThis.console, } = options diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index cbbaad96268753..c7e4e08260c9f8 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -15,7 +15,13 @@ import { } from '../constants' import { perEnvironmentState } from '../environment' import type { Plugin } from '../plugin' -import { escapeRegex, isObject, normalizePath, resolveHostname } from '../utils' +import { + describeConfigOption, + escapeRegex, + isObject, + normalizePath, + resolveHostname, +} from '../utils' import { replaceDefine, serializeDefine } from './define' // ids in transform are normalized to unix style @@ -94,7 +100,12 @@ async function createClientConfigValueReplacer( const protocol = wsConfig?.protocol || null const timeout = wsConfig?.timeout || 30000 const isWsServerSpecified = !!wsConfig?.server - const hmrConfigName = path.basename(config.configFile || 'vite.config.js') + const { option: hmrOverlayOption, file: hmrConfigName } = + describeConfigOption( + config.configHint, + 'server.hmr.overlay', + path.basename(config.configFile || 'vite.config.js'), + ) const hmrConfig = isObject(config.server.hmr) ? config.server.hmr : undefined const overlay = hmrConfig?.overlay !== false @@ -126,10 +137,12 @@ async function createClientConfigValueReplacer( const hmrTimeoutReplacement = escapeReplacement(timeout) const hmrEnableOverlayReplacement = escapeReplacement(overlay) const hmrConfigNameReplacement = escapeReplacement(hmrConfigName) + const hmrOverlayOptionReplacement = escapeReplacement(hmrOverlayOption) const hmrPartialAcceptReplacement = escapeReplacement( config.experimental.hmrPartialAccept, ) const wsTokenReplacement = escapeReplacement(config.webSocketToken) + const displayNameReplacement = escapeReplacement(config.displayName) const serverForwardConsoleReplacement = escapeReplacement( config.server.forwardConsole as any, ) @@ -147,9 +160,11 @@ async function createClientConfigValueReplacer( .replace(`__HMR_TIMEOUT__`, hmrTimeoutReplacement) .replace(`__HMR_ENABLE_OVERLAY__`, hmrEnableOverlayReplacement) .replace(`__HMR_CONFIG_NAME__`, hmrConfigNameReplacement) + .replace(`__HMR_OVERLAY_OPTION__`, hmrOverlayOptionReplacement) .replace(`__HMR_PARTIAL_ACCEPT__`, hmrPartialAcceptReplacement) .replace(`__WS_TOKEN__`, wsTokenReplacement) .replace(`__SERVER_FORWARD_CONSOLE__`, serverForwardConsoleReplacement) + .replace(`__DISPLAY_NAME__`, displayNameReplacement) } const ROLLDOWN_DEV_RUNTIME_ENTRY = 'rolldown/experimental/runtime' diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index 13be7e4345dbe0..5a08534fbca6b1 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -233,7 +233,7 @@ export async function preview( const { allowedHosts } = config.preview // no need to check for HTTPS as HTTPS is not vulnerable to DNS rebinding attacks if (allowedHosts !== true && !config.preview.https) { - app.use(hostValidationMiddleware(allowedHosts, true)) + app.use(hostValidationMiddleware(allowedHosts, true, config.configHint)) } // apply server hooks from plugins diff --git a/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts b/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts index 9dea94a777b491..e077d19dec7837 100644 --- a/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts +++ b/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts @@ -371,6 +371,21 @@ describe('plugin container', () => { } `) }) + + it('names the module a native plugin failed to transform', async () => { + // rolldown's wrapper for its native plugins records the transform + // options (`{ moduleType, ssr, inMap }`) as the failed module's id + const environment = await getDevEnvironment() + + await expect( + environment.pluginContainer.transform('{ "a": 1, }', '/data.json', { + moduleType: 'json', + }), + ).rejects.toMatchObject({ + message: expect.stringContaining('trailing comma'), + id: '/data.json', + }) + }) }) describe('resolveId', () => { diff --git a/packages/vite/src/node/server/bundledDev.ts b/packages/vite/src/node/server/bundledDev.ts index 7eae3ff10534e5..338bb231249713 100644 --- a/packages/vite/src/node/server/bundledDev.ts +++ b/packages/vite/src/node/server/bundledDev.ts @@ -155,7 +155,10 @@ export class BundledDev { client.send({ type: 'error', - err: prepareError(this.lastBuildError), + err: prepareError( + this.lastBuildError, + this.environment.config.hiddenPluginNames, + ), }) } }) @@ -197,7 +200,10 @@ export class BundledDev { for (const client of this.clients.getAll()) { client.send({ type: 'error', - err: prepareError(result), + err: prepareError( + result, + this.environment.config.hiddenPluginNames, + ), }) } return @@ -235,7 +241,10 @@ export class BundledDev { this.fullReloadPending = false this.environment.hot.send({ type: 'error', - err: prepareError(result), + err: prepareError( + result, + this.environment.config.hiddenPluginNames, + ), }) return } diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index b76ad894525bc7..f1457a3abef6b9 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -319,7 +319,12 @@ export class DevEnvironment extends BaseEnvironment { } // Unexpected error, log the issue but avoid an unhandled exception this.logger.error( - buildErrorMessage(e, [`Pre-transform error: ${e.message}`], false), + buildErrorMessage( + e, + [`Pre-transform error: ${e.message}`], + false, + this.config.hiddenPluginNames, + ), { error: e, timestamp: true, diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 5d5eed16112cd1..e268203f3daa9e 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -657,7 +657,7 @@ export async function handleHMRUpdate( } catch (err) { environment.hot.send({ type: 'error', - err: prepareError(err), + err: prepareError(err, environment.config.hiddenPluginNames), }) } } diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 2bfcc96f9e23de..7c25b6b35cb033 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -1016,7 +1016,9 @@ export async function _createServer( const { allowedHosts } = serverConfig // no need to check for HTTPS as HTTPS is not vulnerable to DNS rebinding attacks if (allowedHosts !== true && !serverConfig.https) { - middlewares.use(hostValidationMiddleware(allowedHosts, false)) + middlewares.use( + hostValidationMiddleware(allowedHosts, false, config.configHint), + ) } // apply configureServer hooks ------------------------------------------------ diff --git a/packages/vite/src/node/server/middlewares/__tests__/error.spec.ts b/packages/vite/src/node/server/middlewares/__tests__/error.spec.ts new file mode 100644 index 00000000000000..34d0fe7be408aa --- /dev/null +++ b/packages/vite/src/node/server/middlewares/__tests__/error.spec.ts @@ -0,0 +1,49 @@ +import { stripVTControlCharacters } from 'node:util' +import { describe, expect, test } from 'vitest' +import { buildErrorMessage, prepareError } from '../error' + +function pluginError(plugin: string) { + return Object.assign(new Error('the error'), { plugin, id: '/src/main.js' }) +} + +describe('buildErrorMessage', () => { + test('names the plugin', () => { + expect( + stripVTControlCharacters(buildErrorMessage(pluginError('vite:css'))), + ).toContain(' Plugin: vite:css') + }) + + test('leaves out hidden plugin names', () => { + const message = stripVTControlCharacters( + buildErrorMessage(pluginError('vite:css'), [], false, [/^vite:/]), + ) + expect(message).not.toContain('Plugin:') + expect(message).toContain(' File: /src/main.js') + }) + + test('names plugins that are not hidden', () => { + expect( + stripVTControlCharacters( + buildErrorMessage(pluginError('my-plugin'), [], false, [/^vite:/]), + ), + ).toContain(' Plugin: my-plugin') + }) +}) + +describe('prepareError', () => { + test('keeps the plugin name for the overlay', () => { + expect(prepareError(pluginError('vite:css')).plugin).toBe('vite:css') + }) + + test('leaves out hidden plugin names', () => { + expect( + prepareError(pluginError('vite:css'), [/^vite:/]).plugin, + ).toBeUndefined() + expect(prepareError(pluginError('my-plugin'), ['my-plugin']).plugin).toBe( + undefined, + ) + expect(prepareError(pluginError('my-plugin'), [/^vite:/]).plugin).toBe( + 'my-plugin', + ) + }) +}) diff --git a/packages/vite/src/node/server/middlewares/__tests__/hostCheck.spec.ts b/packages/vite/src/node/server/middlewares/__tests__/hostCheck.spec.ts index 6601cd0c7aed37..f90be06f45b441 100644 --- a/packages/vite/src/node/server/middlewares/__tests__/hostCheck.spec.ts +++ b/packages/vite/src/node/server/middlewares/__tests__/hostCheck.spec.ts @@ -1,5 +1,9 @@ import { expect, test } from 'vitest' -import { getAdditionalAllowedHosts } from '../hostCheck' +import type { ConfigHint } from '../../..' +import { + getAdditionalAllowedHosts, + hostValidationMiddleware, +} from '../hostCheck' test('getAdditionalAllowedHosts', async () => { const actual = getAdditionalAllowedHosts( @@ -23,3 +27,30 @@ test('getAdditionalAllowedHosts', async () => { ].sort(), ) }) + +async function blockedHostMessage(isPreview: boolean, configHint?: ConfigHint) { + let message = '' + await hostValidationMiddleware(['localhost'], isPreview, configHint)( + { headers: { host: 'evil.example.com' } } as any, + { + writeHead: () => undefined, + end: (body: string) => (message = body), + } as any, + () => {}, + ) + return message +} + +test('the blocked-host error names vite.config.js by default', async () => { + expect(await blockedHostMessage(false)).toContain( + 'add "evil.example.com" to `server.allowedHosts` in vite.config.js.', + ) +}) + +test('the blocked-host error names the file and key in configHint', async () => { + expect( + await blockedHostMessage(true, { file: 'my-tool.config.js', key: 'vite' }), + ).toContain( + 'add "evil.example.com" to `vite.preview.allowedHosts` in my-tool.config.js.', + ) +}) diff --git a/packages/vite/src/node/server/middlewares/error.ts b/packages/vite/src/node/server/middlewares/error.ts index 88bfd314a029e2..f50e00c99b5f9e 100644 --- a/packages/vite/src/node/server/middlewares/error.ts +++ b/packages/vite/src/node/server/middlewares/error.ts @@ -6,9 +6,17 @@ import type { Connect } from '#dep-types/connect' import type { ErrorPayload } from '#types/hmrPayload' import type { ViteDevServer } from '../..' import { CLIENT_PUBLIC_PATH } from '../../constants' -import { pad } from '../../utils' +import { isPluginNameHidden, pad } from '../../utils' -export function prepareError(err: Error | RollupError): ErrorPayload['err'] { +/** + * @param hiddenPluginNames the resolved `hiddenPluginNames`, whose plugins + * the error overlay does not name + */ +export function prepareError( + err: Error | RollupError, + hiddenPluginNames: readonly (string | RegExp)[] = [], +): ErrorPayload['err'] { + const plugin = (err as RollupError).plugin // only copy the information we need and avoid serializing unnecessary // properties, since some errors may attach full objects (e.g. PostCSS) return { @@ -16,18 +24,25 @@ export function prepareError(err: Error | RollupError): ErrorPayload['err'] { stack: strip(cleanStack(err.stack || '')), id: (err as RollupError).id, frame: strip((err as RollupError).frame || ''), - plugin: (err as RollupError).plugin, + plugin: isPluginNameHidden(plugin, hiddenPluginNames) ? undefined : plugin, pluginCode: (err as RollupError).pluginCode?.toString(), loc: (err as RollupError).loc, } } +/** + * @param hiddenPluginNames the resolved `hiddenPluginNames`, whose plugins + * the message does not name + */ export function buildErrorMessage( err: RollupError, args: string[] = [], includeStack = true, + hiddenPluginNames: readonly (string | RegExp)[] = [], ): string { - if (err.plugin) args.push(` Plugin: ${colors.magenta(err.plugin)}`) + if (err.plugin && !isPluginNameHidden(err.plugin, hiddenPluginNames)) { + args.push(` Plugin: ${colors.magenta(err.plugin)}`) + } const loc = err.loc ? `:${err.loc.line}:${err.loc.column}` : '' if (err.id) args.push(` File: ${colors.cyan(err.id)}${loc}`) if (err.frame) args.push(colors.yellow(pad(err.frame))) @@ -43,9 +58,13 @@ function cleanStack(stack: string) { } export function logError(server: ViteDevServer, err: RollupError): void { - const msg = buildErrorMessage(err, [ - colors.red(`Internal server error: ${err.message}`), - ]) + const { hiddenPluginNames } = server.config + const msg = buildErrorMessage( + err, + [colors.red(`Internal server error: ${err.message}`)], + true, + hiddenPluginNames, + ) server.config.logger.error(msg, { clear: true, @@ -55,7 +74,7 @@ export function logError(server: ViteDevServer, err: RollupError): void { server.environments.client.hot.send({ type: 'error', - err: prepareError(err), + err: prepareError(err, hiddenPluginNames), }) } @@ -79,10 +98,9 @@ export function errorMiddleware( Error