From 26bcdc3186807bb6f3817119cd7e64ae8308a057 Mon Sep 17 00:00:00 2001 From: Bjorn Lu Date: Tue, 12 Jul 2022 01:27:01 +0800 Subject: [PATCH] feat: expose server resolved urls (#8986) --- docs/guide/migration.md | 2 - .../vite/src/node/__tests__/utils.spec.ts | 15 +-- packages/vite/src/node/index.ts | 3 +- packages/vite/src/node/logger.ts | 109 +++--------------- packages/vite/src/node/preview.ts | 25 +++- packages/vite/src/node/server/index.ts | 54 ++++++--- packages/vite/src/node/utils.ts | 63 +++++++++- 7 files changed, 142 insertions(+), 129 deletions(-) diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 4b0d492c026fea..8047c03c198c80 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -139,8 +139,6 @@ Also there are other breaking changes which only affect few users. - `server.force` option was removed in favor of `optimizeDeps.force` option. - [[#8550] fix: dont handle sigterm in middleware mode](https://github.com/vitejs/vite/pull/8550) - When running in middleware mode, Vite no longer kills process on `SIGTERM`. -- [[#8647] feat: print resolved address for localhost](https://github.com/vitejs/vite/pull/8647) - - `server.printUrls` and `previewServer.printUrls` are now async ## Migration from v1 diff --git a/packages/vite/src/node/__tests__/utils.spec.ts b/packages/vite/src/node/__tests__/utils.spec.ts index a4ffd2b7c917be..51990edf709da2 100644 --- a/packages/vite/src/node/__tests__/utils.spec.ts +++ b/packages/vite/src/node/__tests__/utils.spec.ts @@ -56,8 +56,7 @@ describe('resolveHostname', () => { expect(await resolveHostname(undefined)).toEqual({ host: 'localhost', - name: resolved ?? 'localhost', - implicit: true + name: resolved ?? 'localhost' }) }) @@ -66,24 +65,21 @@ describe('resolveHostname', () => { expect(await resolveHostname('localhost')).toEqual({ host: 'localhost', - name: resolved ?? 'localhost', - implicit: false + name: resolved ?? 'localhost' }) }) test('accepts 0.0.0.0', async () => { expect(await resolveHostname('0.0.0.0')).toEqual({ host: '0.0.0.0', - name: 'localhost', - implicit: false + name: 'localhost' }) }) test('accepts ::', async () => { expect(await resolveHostname('::')).toEqual({ host: '::', - name: 'localhost', - implicit: false + name: 'localhost' }) }) @@ -92,8 +88,7 @@ describe('resolveHostname', () => { await resolveHostname('0000:0000:0000:0000:0000:0000:0000:0000') ).toEqual({ host: '0000:0000:0000:0000:0000:0000:0000:0000', - name: 'localhost', - implicit: false + name: 'localhost' }) }) }) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index bdac099dd214e6..7dfada6825780b 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -17,7 +17,8 @@ export type { ServerOptions, FileSystemServeOptions, ServerHook, - ResolvedServerOptions + ResolvedServerOptions, + ResolvedServerUrls } from './server' export type { BuildOptions, diff --git a/packages/vite/src/node/logger.ts b/packages/vite/src/node/logger.ts index 597de2358f43e3..d0c5d29334c023 100644 --- a/packages/vite/src/node/logger.ts +++ b/packages/vite/src/node/logger.ts @@ -1,15 +1,9 @@ /* eslint no-console: 0 */ -import type { AddressInfo, Server } from 'node:net' -import os from 'node:os' import readline from 'readline' import colors from 'picocolors' import type { RollupError } from 'rollup' -import type { CommonServerOptions } from './http' -import type { Hostname } from './utils' -import { resolveHostname } from './utils' -import { loopbackHosts } from './constants' -import type { ResolvedConfig } from '.' +import type { ResolvedServerUrls } from './server' export type LogType = 'error' | 'warn' | 'info' export type LogLevel = LogType | 'silent' @@ -145,94 +139,23 @@ export function createLogger( return logger } -export async function printCommonServerUrls( - server: Server, - options: CommonServerOptions, - config: ResolvedConfig -): Promise { - const address = server.address() - const isAddressInfo = (x: any): x is AddressInfo => x?.address - if (isAddressInfo(address)) { - const hostname = await resolveHostname(options.host) - const protocol = options.https ? 'https' : 'http' - const base = config.base === './' || config.base === '' ? '/' : config.base - printServerUrls(hostname, protocol, address.port, base, config.logger.info) - } -} - -function printServerUrls( - hostname: Hostname, - protocol: string, - port: number, - base: string, +export function printServerUrls( + urls: ResolvedServerUrls, + optionsHost: string | boolean | undefined, info: Logger['info'] ): void { - const urls: Array<{ label: string; url: string; disabled?: boolean }> = [] - const notes: Array<{ label: string; message: string }> = [] - - if (hostname.host && loopbackHosts.has(hostname.host)) { - let hostnameName = hostname.name - if ( - hostnameName === '::1' || - hostnameName === '0000:0000:0000:0000:0000:0000:0000:0001' - ) { - hostnameName = `[${hostnameName}]` - } - - urls.push({ - label: 'Local', - url: colors.cyan( - `${protocol}://${hostnameName}:${colors.bold(port)}${base}` - ) - }) - - if (hostname.implicit) { - urls.push({ - label: 'Network', - url: `use ${colors.white(colors.bold('--host'))} to expose`, - disabled: true - }) - } - } else { - Object.values(os.networkInterfaces()) - .flatMap((nInterface) => nInterface ?? []) - .filter( - (detail) => - detail && - detail.address && - // Node < v18 - ((typeof detail.family === 'string' && detail.family === 'IPv4') || - // Node >= v18 - (typeof detail.family === 'number' && detail.family === 4)) - ) - .forEach((detail) => { - const host = detail.address.replace('127.0.0.1', hostname.name) - const url = `${protocol}://${host}:${colors.bold(port)}${base}` - const label = detail.address.includes('127.0.0.1') ? 'Local' : 'Network' - - urls.push({ label, url: colors.cyan(url) }) - }) + const colorUrl = (url: string) => + colors.cyan(url.replace(/:(\d+)\//, (_, port) => `:${colors.bold(port)}/`)) + for (const url of urls.local) { + info(` ${colors.green('➜')} ${colors.bold('Local')}: ${colorUrl(url)}`) } - - const length = Math.max( - ...[...urls, ...notes].map(({ label }) => label.length) - ) - const print = ( - iconWithColor: string, - label: string, - messageWithColor: string, - disabled?: boolean - ) => { - const message = ` ${iconWithColor} ${ - label ? colors.bold(label) + ':' : ' ' - } ${' '.repeat(length - label.length)}${messageWithColor}` - info(disabled ? colors.dim(message) : message) + for (const url of urls.network) { + info(` ${colors.green('➜')} ${colors.bold('Network')}: ${colorUrl(url)}`) + } + if (urls.network.length === 0 && optionsHost === undefined) { + const note = `use ${colors.white(colors.bold('--host'))} to expose` + info( + colors.dim(` ${colors.green('➜')} ${colors.bold('Network')}: ${note}`) + ) } - - urls.forEach(({ label, url: text, disabled }) => { - print(colors.green('➜'), label, text, disabled) - }) - notes.forEach(({ label, message: text }) => { - print(colors.white('❖'), label, text) - }) } diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index 5820a3bc6cdb91..7b2cc4bb1729b4 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -4,14 +4,14 @@ import sirv from 'sirv' import connect from 'connect' import type { Connect } from 'types/connect' import corsMiddleware from 'cors' -import type { ResolvedServerOptions } from './server' +import type { ResolvedServerOptions, ResolvedServerUrls } from './server' import type { CommonServerOptions } from './http' import { httpServerStart, resolveHttpServer, resolveHttpsConfig } from './http' import { openBrowser } from './server/openBrowser' import compression from './server/middlewares/compression' import { proxyMiddleware } from './server/middlewares/proxy' -import { resolveHostname } from './utils' -import { printCommonServerUrls } from './logger' +import { resolveHostname, resolveServerUrls } from './utils' +import { printServerUrls } from './logger' import { resolveConfig } from '.' import type { InlineConfig, ResolvedConfig } from '.' @@ -47,10 +47,16 @@ export interface PreviewServer { * native Node http server instance */ httpServer: http.Server + /** + * The resolved urls Vite prints on the + * + * @experimental + */ + resolvedUrls: ResolvedServerUrls /** * Print server urls */ - printUrls: () => Promise + printUrls(): void } export type PreviewServerHook = (server: { @@ -127,6 +133,12 @@ export async function preview( logger }) + const resolvedUrls = await resolveServerUrls( + httpServer, + config.preview, + config + ) + if (options.open) { const path = typeof options.open === 'string' ? options.open : previewBase openBrowser( @@ -141,8 +153,9 @@ export async function preview( return { config, httpServer, - async printUrls() { - await printCommonServerUrls(httpServer, config.preview, config) + resolvedUrls, + printUrls() { + printServerUrls(resolvedUrls, options.host, logger.info) } } } diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 8fe8f00b6d8165..a8222be65ae7b2 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -19,7 +19,8 @@ import { isParentDirectory, mergeConfig, normalizePath, - resolveHostname + resolveHostname, + resolveServerUrls } from '../utils' import { ssrLoadModule } from '../ssr/ssrModuleLoader' import { cjsSsrResolveExternals } from '../ssr/ssrExternal' @@ -35,7 +36,7 @@ import { } from '../optimizer' import { CLIENT_DIR } from '../constants' import type { Logger } from '../logger' -import { printCommonServerUrls } from '../logger' +import { printServerUrls } from '../logger' import { invalidatePackageData } from '../packages' import type { PluginContainer } from './pluginContainer' import { createPluginContainer } from './pluginContainer' @@ -184,6 +185,13 @@ export interface ViteDevServer { * and hmr state. */ moduleGraph: ModuleGraph + /** + * The resolved urls Vite prints on the CLI. null in middleware mode or + * before `server.listen` is called. + * + * @experimental + */ + resolvedUrls: ResolvedServerUrls | null /** * Programmatically resolve, load and transform a URL and get the result * without going through the http request pipeline. @@ -234,7 +242,7 @@ export interface ViteDevServer { /** * Print server urls */ - printUrls(): Promise + printUrls(): void /** * Restart the server. * @@ -271,6 +279,11 @@ export interface ViteDevServer { > } +export interface ResolvedServerUrls { + local: string[] + network: string[] +} + export async function createServer( inlineConfig: InlineConfig = {} ): Promise { @@ -319,6 +332,7 @@ export async function createServer( pluginContainer: container, ws, moduleGraph, + resolvedUrls: null, // will be set on listen ssrTransform(code: string, inMap: SourceMap | null, url: string) { return ssrTransform(code, inMap, url, code, { json: { stringify: server.config.json?.stringify } @@ -350,8 +364,16 @@ export async function createServer( ssrRewriteStacktrace(stack: string) { return ssrRewriteStacktrace(stack, moduleGraph) }, - listen(port?: number, isRestart?: boolean) { - return startServer(server, port, isRestart) + async listen(port?: number, isRestart?: boolean) { + await startServer(server, port, isRestart) + if (httpServer) { + server.resolvedUrls = await resolveServerUrls( + httpServer, + config.server, + config + ) + } + return server }, async close() { if (!middlewareMode) { @@ -360,19 +382,27 @@ export async function createServer( process.stdin.off('end', exitProcess) } } - await Promise.all([ watcher.close(), ws.close(), container.close(), closeHttpServer() ]) + server.resolvedUrls = null }, - async printUrls() { - if (httpServer) { - await printCommonServerUrls(httpServer, config.server, config) - } else { + printUrls() { + if (server.resolvedUrls) { + printServerUrls( + server.resolvedUrls, + serverConfig.host, + config.logger.info + ) + } else if (middlewareMode) { throw new Error('cannot print server URLs in middleware mode.') + } else { + throw new Error( + 'cannot print server URLs before server.listen is called.' + ) } }, async restart(forceOptimize?: boolean) { @@ -572,7 +602,7 @@ async function startServer( server: ViteDevServer, inlinePort?: number, isRestart: boolean = false -): Promise { +): Promise { const httpServer = server.httpServer if (!httpServer) { throw new Error('Cannot call server.listen in middleware mode.') @@ -622,8 +652,6 @@ async function startServer( server.config.logger ) } - - return server } function createServerCloseFn(server: http.Server | null) { diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 0b0c205d80b9e4..1d647e366a3be2 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -7,6 +7,7 @@ import { URL, URLSearchParams, pathToFileURL } from 'node:url' import { builtinModules, createRequire } from 'node:module' import { promises as dns } from 'node:dns' import { performance } from 'node:perf_hooks' +import type { AddressInfo, Server } from 'node:net' import resolve from 'resolve' import type { FSWatcher } from 'chokidar' import remapping from '@ampproject/remapping' @@ -26,10 +27,13 @@ import { FS_PREFIX, OPTIMIZABLE_ENTRY_RE, VALID_ID_PREFIX, + loopbackHosts, wildcardHosts } from './constants' import type { DepOptimizationConfig } from './optimizer' -import type { ResolvedConfig } from '.' +import type { ResolvedConfig } from './config' +import type { ResolvedServerUrls } from './server' +import type { CommonServerOptions } from '.' /** * Inlined to keep `@rollup/pluginutils` in devDependencies @@ -770,8 +774,6 @@ export interface Hostname { host: string | undefined /** resolve to localhost when possible */ name: string - /** if it is using the default behavior */ - implicit: boolean } export async function resolveHostname( @@ -799,7 +801,60 @@ export async function resolveHostname( } } - return { host, name, implicit: optionsHost === undefined } + return { host, name } +} + +export async function resolveServerUrls( + server: Server, + options: CommonServerOptions, + config: ResolvedConfig +): Promise { + const address = server.address() + + const isAddressInfo = (x: any): x is AddressInfo => x?.address + if (!isAddressInfo(address)) { + return { local: [], network: [] } + } + + const local: string[] = [] + const network: string[] = [] + const hostname = await resolveHostname(options.host) + const protocol = options.https ? 'https' : 'http' + const port = address.port + const base = config.base === './' || config.base === '' ? '/' : config.base + + if (hostname.host && loopbackHosts.has(hostname.host)) { + let hostnameName = hostname.name + if ( + hostnameName === '::1' || + hostnameName === '0000:0000:0000:0000:0000:0000:0000:0001' + ) { + hostnameName = `[${hostnameName}]` + } + local.push(`${protocol}://${hostnameName}:${port}${base}`) + } else { + Object.values(os.networkInterfaces()) + .flatMap((nInterface) => nInterface ?? []) + .filter( + (detail) => + detail && + detail.address && + // Node < v18 + ((typeof detail.family === 'string' && detail.family === 'IPv4') || + // Node >= v18 + (typeof detail.family === 'number' && detail.family === 4)) + ) + .forEach((detail) => { + const host = detail.address.replace('127.0.0.1', hostname.name) + const url = `${protocol}://${host}:${port}${base}` + if (detail.address.includes('127.0.0.1')) { + local.push(url) + } else { + network.push(url) + } + }) + } + return { local, network } } export function arraify(target: T | T[]): T[] {