From eb52d36a2cbdf6182691ac3ec5cb6d28d1ac73ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=A0=20/=20green?= Date: Mon, 20 Jun 2022 14:18:42 +0900 Subject: [PATCH] feat: print resolved address for localhost (#8647) Co-authored-by: Bjorn Lu --- docs/config/server-options.md | 2 +- docs/guide/migration.md | 2 + .../vite/src/node/__tests__/utils.spec.ts | 42 +++++++++++------- packages/vite/src/node/logger.ts | 8 ++-- packages/vite/src/node/preview.ts | 8 ++-- packages/vite/src/node/server/index.ts | 8 ++-- packages/vite/src/node/utils.ts | 44 ++++++++++++++++--- playground/cli-module/__tests__/serve.ts | 4 +- playground/cli/__tests__/serve.ts | 4 +- 9 files changed, 85 insertions(+), 37 deletions(-) diff --git a/docs/config/server-options.md b/docs/config/server-options.md index 9fe1f22ac5b63a..33f8bb03121b27 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -14,7 +14,7 @@ This can be set via the CLI using `--host 0.0.0.0` or `--host`. There are cases when other servers might respond instead of Vite. -The first case is when `localhost` is used. Node.js below v17 reorders the result of DNS-resolved address by default. When accessing `localhost`, browsers use DNS to resolve the address and that address might differ from the address which Vite is listening. +The first case is when `localhost` is used. Node.js below v17 reorders the result of DNS-resolved address by default. When accessing `localhost`, browsers use DNS to resolve the address and that address might differ from the address which Vite is listening. Vite prints the resolved address when it differs. You could set [`dns.setDefaultResultOrder('verbatim')`](https://nodejs.org/api/dns.html#dns_dns_setdefaultresultorder_order) to disable the reordering behavior. Or you could set `server.host` to `127.0.0.1` explicitly. diff --git a/docs/guide/migration.md b/docs/guide/migration.md index ca18af47e011fa..d33154ad6cec08 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -134,6 +134,8 @@ 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 889cbf8386538b..a4ffd2b7c917be 100644 --- a/packages/vite/src/node/__tests__/utils.spec.ts +++ b/packages/vite/src/node/__tests__/utils.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from 'vitest' import { asyncFlatten, getHash, + getLocalhostAddressIfDiffersFromDNS, getPotentialTsSrcPaths, injectQuery, isWindows, @@ -50,38 +51,49 @@ describe('injectQuery', () => { }) describe('resolveHostname', () => { - test('defaults to localhost', () => { - expect(resolveHostname(undefined)).toEqual({ + test('defaults to localhost', async () => { + const resolved = await getLocalhostAddressIfDiffersFromDNS() + + expect(await resolveHostname(undefined)).toEqual({ host: 'localhost', - name: 'localhost' + name: resolved ?? 'localhost', + implicit: true }) }) - test('accepts localhost', () => { - expect(resolveHostname('localhost')).toEqual({ + test('accepts localhost', async () => { + const resolved = await getLocalhostAddressIfDiffersFromDNS() + + expect(await resolveHostname('localhost')).toEqual({ host: 'localhost', - name: 'localhost' + name: resolved ?? 'localhost', + implicit: false }) }) - test('accepts 0.0.0.0', () => { - expect(resolveHostname('0.0.0.0')).toEqual({ + test('accepts 0.0.0.0', async () => { + expect(await resolveHostname('0.0.0.0')).toEqual({ host: '0.0.0.0', - name: 'localhost' + name: 'localhost', + implicit: false }) }) - test('accepts ::', () => { - expect(resolveHostname('::')).toEqual({ + test('accepts ::', async () => { + expect(await resolveHostname('::')).toEqual({ host: '::', - name: 'localhost' + name: 'localhost', + implicit: false }) }) - test('accepts 0000:0000:0000:0000:0000:0000:0000:0000', () => { - expect(resolveHostname('0000:0000:0000:0000:0000:0000:0000:0000')).toEqual({ + test('accepts 0000:0000:0000:0000:0000:0000:0000:0000', async () => { + expect( + await resolveHostname('0000:0000:0000:0000:0000:0000:0000:0000') + ).toEqual({ host: '0000:0000:0000:0000:0000:0000:0000:0000', - name: 'localhost' + name: 'localhost', + implicit: false }) }) }) diff --git a/packages/vite/src/node/logger.ts b/packages/vite/src/node/logger.ts index d2b33397fbfd9e..edb57b02fc2cdb 100644 --- a/packages/vite/src/node/logger.ts +++ b/packages/vite/src/node/logger.ts @@ -145,15 +145,15 @@ export function createLogger( return logger } -export function printCommonServerUrls( +export async function printCommonServerUrls( server: Server, options: CommonServerOptions, config: ResolvedConfig -): void { +): Promise { const address = server.address() const isAddressInfo = (x: any): x is AddressInfo => x?.address if (isAddressInfo(address)) { - const hostname = resolveHostname(options.host) + const hostname = await resolveHostname(options.host) const protocol = options.https ? 'https' : 'http' printServerUrls( hostname, @@ -191,7 +191,7 @@ function printServerUrls( ) }) - if (hostname.name === 'localhost') { + if (hostname.implicit) { urls.push({ label: 'Network', url: `use ${colors.white(colors.bold('--host'))} to expose`, diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index feb106ad086709..dd3010f9a21aa1 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -50,7 +50,7 @@ export interface PreviewServer { /** * Print server urls */ - printUrls: () => void + printUrls: () => Promise } export type PreviewServerHook = (server: { @@ -112,7 +112,7 @@ export async function preview( postHooks.forEach((fn) => fn && fn()) const options = config.preview - const hostname = resolveHostname(options.host) + const hostname = await resolveHostname(options.host) const port = options.port ?? 4173 const protocol = options.https ? 'https' : 'http' const logger = config.logger @@ -139,8 +139,8 @@ export async function preview( return { config, httpServer, - printUrls() { - printCommonServerUrls(httpServer, config.preview, config) + async printUrls() { + await printCommonServerUrls(httpServer, config.preview, config) } } } diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 7ef5d2f341920e..1ce92f73274148 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -223,7 +223,7 @@ export interface ViteDevServer { /** * Print server urls */ - printUrls(): void + printUrls(): Promise /** * Restart the server. * @@ -353,9 +353,9 @@ export async function createServer( closeHttpServer() ]) }, - printUrls() { + async printUrls() { if (httpServer) { - printCommonServerUrls(httpServer, config.server, config) + await printCommonServerUrls(httpServer, config.server, config) } else { throw new Error('cannot print server URLs in middleware mode.') } @@ -555,7 +555,7 @@ async function startServer( const options = server.config.server const port = inlinePort ?? options.port ?? 5173 - const hostname = resolveHostname(options.host) + const hostname = await resolveHostname(options.host) const protocol = options.https ? 'https' : 'http' const info = server.config.logger.info diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 4583147628903f..caf2e96c71914e 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -5,6 +5,7 @@ import { createHash } from 'node:crypto' import { promisify } from 'node:util' 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 resolve from 'resolve' import type { FSWatcher } from 'chokidar' @@ -735,16 +736,38 @@ export function unique(arr: T[]): T[] { return Array.from(new Set(arr)) } +/** + * Returns resolved localhost address when `dns.lookup` result differs from DNS + * + * `dns.lookup` result is same when defaultResultOrder is `verbatim`. + * Even if defaultResultOrder is `ipv4first`, `dns.lookup` result maybe same. + * For example, when IPv6 is not supported on that machine/network. + */ +export async function getLocalhostAddressIfDiffersFromDNS(): Promise< + string | undefined +> { + const [nodeResult, dnsResult] = await Promise.all([ + dns.lookup('localhost'), + dns.lookup('localhost', { verbatim: true }) + ]) + const isSame = + nodeResult.family === dnsResult.family && + nodeResult.address === dnsResult.address + return isSame ? undefined : nodeResult.address +} + export interface Hostname { - // undefined sets the default behaviour of server.listen + /** undefined sets the default behaviour of server.listen */ host: string | undefined - // resolve to localhost when possible + /** resolve to localhost when possible */ name: string + /** if it is using the default behavior */ + implicit: boolean } -export function resolveHostname( +export async function resolveHostname( optionsHost: string | boolean | undefined -): Hostname { +): Promise { let host: string | undefined if (optionsHost === undefined || optionsHost === false) { // Use a secure default @@ -757,10 +780,17 @@ export function resolveHostname( } // Set host name to localhost when possible - const name = - host === undefined || wildcardHosts.has(host) ? 'localhost' : host + let name = host === undefined || wildcardHosts.has(host) ? 'localhost' : host + + if (host === 'localhost') { + // See #8647 for more details. + const localhostAddr = await getLocalhostAddressIfDiffersFromDNS() + if (localhostAddr) { + name = localhostAddr + } + } - return { host, name } + return { host, name, implicit: optionsHost === undefined } } export function arraify(target: T | T[]): T[] { diff --git a/playground/cli-module/__tests__/serve.ts b/playground/cli-module/__tests__/serve.ts index 698cbd048851d1..40921d17a255b4 100644 --- a/playground/cli-module/__tests__/serve.ts +++ b/playground/cli-module/__tests__/serve.ts @@ -119,7 +119,9 @@ async function startedOnPort(serverProcess, port, timeout) { const str = data.toString() // hack, console output may contain color code gibberish // skip gibberish between localhost: and port number - const match = str.match(/(http:\/\/localhost:)(?:.*)(\d{4})/) + const match = str.match( + /(http:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):)(?:.*)(\d{4})/ + ) if (match) { const startedPort = parseInt(match[2], 10) if (startedPort === port) { diff --git a/playground/cli/__tests__/serve.ts b/playground/cli/__tests__/serve.ts index 66f00677169f2a..f0d34219be68fa 100644 --- a/playground/cli/__tests__/serve.ts +++ b/playground/cli/__tests__/serve.ts @@ -119,7 +119,9 @@ async function startedOnPort(serverProcess, port, timeout) { const str = data.toString() // hack, console output may contain color code gibberish // skip gibberish between localhost: and port number - const match = str.match(/(http:\/\/localhost:)(?:.*)(\d{4})/) + const match = str.match( + /(http:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):)(?:.*)(\d{4})/ + ) if (match) { const startedPort = parseInt(match[2], 10) if (startedPort === port) {