diff --git a/.changeset/real-hounds-tan.md b/.changeset/real-hounds-tan.md new file mode 100644 index 0000000000..6a515f46a1 --- /dev/null +++ b/.changeset/real-hounds-tan.md @@ -0,0 +1,7 @@ +--- +"@lynx-js/rspeedy": patch +--- + +Select the most appropriate network interface. + +This is a port of [webpack/webpack-dev-server#5411](https://github.com/webpack/webpack-dev-server/pull/5411). diff --git a/packages/rspeedy/core/src/plugins/dev.plugin.ts b/packages/rspeedy/core/src/plugins/dev.plugin.ts index b35b56d76c..888824beed 100644 --- a/packages/rspeedy/core/src/plugins/dev.plugin.ts +++ b/packages/rspeedy/core/src/plugins/dev.plugin.ts @@ -167,7 +167,7 @@ export async function findIp( let host: string | undefined - Object.values(os.networkInterfaces()) + const networks = Object.values(os.networkInterfaces()) .flatMap((networks) => networks ?? []) .filter((network) => { if (!network || !network.address) { @@ -192,12 +192,16 @@ export async function findIp( return network.address }) - .forEach((network) => { - host = network.address - if (host.includes(':')) { - host = `[${host}]` - } - }) + + if (networks.length > 0) { + // Take the first network found + // See: https://github.com/webpack/webpack-dev-server/pull/5411/ + host = networks[0]!.address + + if (host.includes(':')) { + host = `[${host}]` + } + } if (!host) { throw new Error(`No valid IP found`) diff --git a/packages/rspeedy/core/test/plugins/dev.plugin.test.ts b/packages/rspeedy/core/test/plugins/dev.plugin.test.ts index a6bbb30b24..cf8422ec85 100644 --- a/packages/rspeedy/core/test/plugins/dev.plugin.test.ts +++ b/packages/rspeedy/core/test/plugins/dev.plugin.test.ts @@ -9,11 +9,28 @@ import { assert, beforeEach, describe, expect, test, vi } from 'vitest' import { createStubRspeedy } from '../createStubRspeedy.js' +vi.mock('node:os') + describe('Plugins - Dev', () => { - beforeEach(() => { + beforeEach(async () => { vi.stubEnv('NODE_ENV', 'development') vi.mock('../../src/webpack/ProvidePlugin.js') + const { default: os } = await import('node:os') + + vi.mocked(os.networkInterfaces).mockReturnValue({ + eth0: [ + { + address: '192.168.1.1', + family: 'IPv4', + internal: false, + netmask: '255.255.255.0', + mac: '00:00:00:00:00:00', + cidr: '192.168.1.1/24', + }, + ], + }) + return () => { vi.unstubAllEnvs() .restoreAllMocks() diff --git a/packages/rspeedy/core/test/plugins/findIp.test.ts b/packages/rspeedy/core/test/plugins/findIp.test.ts new file mode 100644 index 0000000000..3aba0a0cba --- /dev/null +++ b/packages/rspeedy/core/test/plugins/findIp.test.ts @@ -0,0 +1,184 @@ +// Copyright 2024 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. + +import { describe, expect, test, vi } from 'vitest' + +vi.mock('node:os') + +describe('findIp', () => { + test('v4', async () => { + const { default: os } = await import('node:os') + + vi.mocked(os.networkInterfaces).mockReturnValue({ + eth0: [ + { + address: '192.168.1.1', + family: 'IPv4', + internal: false, + netmask: '255.255.255.0', + mac: '00:00:00:00:00:00', + cidr: '192.168.1.1/24', + }, + ], + }) + + const { findIp } = await import('../../src/plugins/dev.plugin.js') + + const ip = await findIp('v4') + expect(ip).toBe('192.168.1.1') + }) + + test('v6', async () => { + const { default: os } = await import('node:os') + + vi.mocked(os.networkInterfaces).mockReturnValue({ + eth0: [ + { + address: 'fd00::1', + family: 'IPv6', + internal: false, + netmask: 'ffff:ffff:ffff:ffff::', + mac: '00:00:00:00:00:00', + cidr: 'fd00::1/64', + scopeid: 0, + }, + ], + }) + + const { findIp } = await import('../../src/plugins/dev.plugin.js') + + const ip = await findIp('v6') + expect(ip).toBe('[fd00::1]') + }) + + test('multiple ips (should use the first ip)', async () => { + const { default: os } = await import('node:os') + + vi.mocked(os.networkInterfaces).mockReturnValue({ + eth0: [ + { + address: '192.168.1.1', + family: 'IPv4', + internal: false, + netmask: '255.255.255.0', + mac: '00:00:00:00:00:00', + cidr: '192.168.1.1/24', + }, + { + address: '192.168.2.1', + family: 'IPv4', + internal: false, + netmask: '255.255.255.0', + mac: '00:00:00:00:00:00', + cidr: '192.168.2.1/24', + }, + ], + }) + + const { findIp } = await import('../../src/plugins/dev.plugin.js') + + const ip = await findIp('v4') + expect(ip).toBe('192.168.1.1') // should use the first ip + }) + + test('multiple ips (should ignore internal ips)', async () => { + const { default: os } = await import('node:os') + + vi.mocked(os.networkInterfaces).mockReturnValue({ + eth0: [ + { + address: '192.168.2.1', + family: 'IPv4', + internal: true, + netmask: '255.255.255.0', + mac: '00:00:00:00:00:00', + cidr: '192.168.2.1/24', + }, + { + address: '192.168.1.1', + family: 'IPv4', + internal: false, + netmask: '255.255.255.0', + mac: '00:00:00:00:00:00', + cidr: '192.168.1.1/24', + }, + ], + }) + + const { findIp } = await import('../../src/plugins/dev.plugin.js') + + const ip = await findIp('v4') + expect(ip).toBe('192.168.1.1') // should ignore internal ips + }) + + test('no v4 ips', async () => { + const { default: os } = await import('node:os') + + vi.mocked(os.networkInterfaces).mockReturnValue({ + eth0: [ + { + address: 'fd00::1', + family: 'IPv6', + internal: false, + netmask: 'ffff:ffff:ffff:ffff::', + mac: '00:00:00:00:00:00', + cidr: 'fd00::1/64', + scopeid: 0, + }, + ], + }) + + const { findIp } = await import('../../src/plugins/dev.plugin.js') + + await expect(findIp('v4')).rejects.toThrow( + 'No valid IP found', + ) + }) + + test('no ips', async () => { + const { default: os } = await import('node:os') + + vi.mocked(os.networkInterfaces).mockReturnValue({}) + + const { findIp } = await import('../../src/plugins/dev.plugin.js') + + await expect(findIp('v4')).rejects.toThrow( + 'No valid IP found', + ) + }) + + test('invalid network interfaces', async () => { + const { default: os } = await import('node:os') + + vi.mocked(os.networkInterfaces).mockReturnValue({ + // @ts-expect-error mocked invalid network interfaces + eth0: null, + }) + + const { findIp } = await import('../../src/plugins/dev.plugin.js') + + await expect(findIp('v4')).rejects.toThrow( + 'No valid IP found', + ) + }) + + test('invalid ip address', async () => { + const { default: os } = await import('node:os') + + vi.mocked(os.networkInterfaces).mockReturnValue({ + eth0: [ + { + // @ts-expect-error invalid ip address + address: null, + }, + ], + }) + + const { findIp } = await import('../../src/plugins/dev.plugin.js') + + await expect(findIp('v4')).rejects.toThrow( + 'No valid IP found', + ) + }) +})