diff --git a/packages/dart_frog_cli/lib/src/commands/dev/dev.dart b/packages/dart_frog_cli/lib/src/commands/dev/dev.dart index 884fcb4ad..b3ba5e866 100644 --- a/packages/dart_frog_cli/lib/src/commands/dev/dev.dart +++ b/packages/dart_frog_cli/lib/src/commands/dev/dev.dart @@ -37,6 +37,12 @@ class DevCommand extends DartFrogCommand { abbr: 'd', defaultsTo: _defaultDartVmServicePort, help: 'Which port number the dart vm service should listen on.', + ) + ..addOption( + 'hostname', + abbr: 'H', + help: 'Which host name the server should bind to.', + defaultsTo: 'localhost', ); } @@ -106,15 +112,30 @@ class DevCommand extends DartFrogCommand { _ensureRuntimeCompatibility(cwd); final port = io.Platform.environment['PORT'] ?? results['port'] as String; + final dartVmServicePort = (results['dart-vm-service-port'] as String?) ?? _defaultDartVmServicePort; final generator = await _generator(dartFrogDevServerBundle); + final hostname = results['hostname'] as String?; + + io.InternetAddress? ip; + if (hostname != null && hostname != 'localhost') { + ip = io.InternetAddress.tryParse(hostname); + if (ip == null) { + logger.err( + 'Invalid hostname "$hostname": must be a valid IPv4 or IPv6 address.', + ); + return ExitCode.software.code; + } + } + _devServerRunner = _devServerRunnerBuilder( devServerBundleGenerator: generator, logger: logger, workingDirectory: cwd, port: port, + address: ip, dartVmServicePort: dartVmServicePort, onHotReloadEnabled: _startListeningForHelpers, ); diff --git a/packages/dart_frog_cli/lib/src/daemon/domain/dev_server_domain.dart b/packages/dart_frog_cli/lib/src/daemon/domain/dev_server_domain.dart index 2eebacc8e..136296008 100644 --- a/packages/dart_frog_cli/lib/src/daemon/domain/dev_server_domain.dart +++ b/packages/dart_frog_cli/lib/src/daemon/domain/dev_server_domain.dart @@ -42,6 +42,8 @@ class DevServerDomain extends DomainBase { final dartVmServicePort = request.getParam('dartVmServicePort'); + final hostname = request.getParam('hostname'); + final applicationId = getId(); daemon.sendEvent( @@ -57,6 +59,16 @@ class DevServerDomain extends DomainBase { final devServerBundleGenerator = await _generator(dartFrogDevServerBundle); + InternetAddress? ip; + if (hostname != null) { + ip = InternetAddress.tryParse(hostname); + if (ip == null) { + throw DartFrogDaemonMalformedMessageException( + 'invalid hostname "$hostname": must be a valid IPv4 or IPv6 address.', + ); + } + } + final logger = DaemonLogger( domain: domainName, params: { @@ -71,6 +83,7 @@ class DevServerDomain extends DomainBase { final devServerRunner = _devServerRunnerBuilder( logger: logger, port: '$port', + address: ip, devServerBundleGenerator: devServerBundleGenerator, dartVmServicePort: '$dartVmServicePort', workingDirectory: Directory(workingDirectory), diff --git a/packages/dart_frog_cli/lib/src/dev_server_runner/dev_server_runner.dart b/packages/dart_frog_cli/lib/src/dev_server_runner/dev_server_runner.dart index 1f223ac65..756ff334c 100644 --- a/packages/dart_frog_cli/lib/src/dev_server_runner/dev_server_runner.dart +++ b/packages/dart_frog_cli/lib/src/dev_server_runner/dev_server_runner.dart @@ -41,6 +41,7 @@ final _dartVmServiceAlreadyInUseErrorRegex = RegExp( typedef DevServerRunnerBuilder = DevServerRunner Function({ required Logger logger, required String port, + required io.InternetAddress? address, required MasonGenerator devServerBundleGenerator, required String dartVmServicePort, required io.Directory workingDirectory, @@ -65,6 +66,7 @@ class DevServerRunner { DevServerRunner({ required this.logger, required this.port, + required this.address, required this.devServerBundleGenerator, required this.dartVmServicePort, required this.workingDirectory, @@ -95,6 +97,12 @@ class DevServerRunner { /// Which port number the server should start on. final String port; + /// Which host the server should start on. + /// Which host the server should start on. + /// + /// It will default to localhost if empty. + final io.InternetAddress? address; + /// Which port number the dart vm service should listen on. final String dartVmServicePort; @@ -142,7 +150,12 @@ class DevServerRunner { Future _codegen() async { logger.detail('[codegen] running pre-gen...'); - var vars = {'port': port}; + final address = this.address; + logger.detail('Starting development server on host ${address?.address}'); + var vars = { + 'port': port, + if (address != null) 'host': address.address, + }; await devServerBundleGenerator.hooks.preGen( vars: vars, workingDirectory: workingDirectory.path, @@ -322,7 +335,9 @@ class DevServerRunner { await _codegen(); await serve(); - final localhost = link(uri: Uri.parse('http://localhost:$port')); + final hostAddress = address?.address ?? 'localhost'; + + final localhost = link(uri: Uri.parse('http://$hostAddress:$port')); progress.complete('Running on $localhost'); final cwdPath = workingDirectory.path; diff --git a/packages/dart_frog_cli/test/src/commands/dev/dev_test.dart b/packages/dart_frog_cli/test/src/commands/dev/dev_test.dart index 0c30ee854..52b26d059 100644 --- a/packages/dart_frog_cli/test/src/commands/dev/dev_test.dart +++ b/packages/dart_frog_cli/test/src/commands/dev/dev_test.dart @@ -54,6 +54,7 @@ void main() { devServerRunnerBuilder: ({ required logger, required port, + required address, required devServerBundleGenerator, required dartVmServicePort, required workingDirectory, @@ -78,6 +79,7 @@ void main() { (_) => Future.value(ExitCode.success), ); + when(() => argResults['hostname']).thenReturn('192.168.1.2'); when(() => argResults['port']).thenReturn('1234'); when(() => argResults['dart-vm-service-port']).thenReturn('5678'); @@ -85,6 +87,7 @@ void main() { late String givenPort; late String givenDartVmServicePort; + late InternetAddress? givenAddress; late MasonGenerator givenDevServerBundleGenerator; late Directory givenWorkingDirectory; late void Function()? givenOnHotReloadEnabled; @@ -95,12 +98,14 @@ void main() { devServerRunnerBuilder: ({ required logger, required port, + required address, required devServerBundleGenerator, required dartVmServicePort, required workingDirectory, void Function()? onHotReloadEnabled, }) { givenPort = port; + givenAddress = address; givenDartVmServicePort = dartVmServicePort; givenDevServerBundleGenerator = devServerBundleGenerator; givenWorkingDirectory = workingDirectory; @@ -118,6 +123,7 @@ void main() { verify(() => runner.start()).called(1); expect(givenPort, equals('1234')); + expect(givenAddress, InternetAddress.tryParse('192.168.1.2')); expect(givenDartVmServicePort, equals('5678')); expect(givenDevServerBundleGenerator, same(generator)); expect(givenWorkingDirectory, same(cwd)); @@ -131,6 +137,7 @@ void main() { devServerRunnerBuilder: ({ required logger, required port, + required address, required devServerBundleGenerator, required dartVmServicePort, required workingDirectory, @@ -161,6 +168,7 @@ void main() { devServerRunnerBuilder: ({ required logger, required port, + required address, required devServerBundleGenerator, required dartVmServicePort, required workingDirectory, @@ -181,6 +189,49 @@ void main() { verify(() => logger.err('oops')).called(1); }); + test('fails if hostname is invalid', () async { + when(() => runner.start()).thenAnswer((_) => Future.value()); + when(() => runner.exitCode).thenAnswer( + (_) => Future.value(ExitCode.success), + ); + + when(() => argResults['hostname']).thenReturn('ticarica'); + when(() => argResults['port']).thenReturn('1234'); + when(() => argResults['dart-vm-service-port']).thenReturn('5678'); + + final cwd = Directory.systemTemp; + + final command = DevCommand( + generator: (_) async => generator, + ensureRuntimeCompatibility: (_) {}, + devServerRunnerBuilder: ({ + required logger, + required port, + required address, + required devServerBundleGenerator, + required dartVmServicePort, + required workingDirectory, + void Function()? onHotReloadEnabled, + }) { + return runner; + }, + logger: logger, + ) + ..testStdin = stdin + ..testArgResults = argResults + ..testCwd = cwd; + + await expectLater(command.run(), completion(ExitCode.software.code)); + + verify( + () => logger.err( + 'Invalid hostname "ticarica": must be a valid IPv4 or IPv6 address.', + ), + ).called(1); + + verifyNever(() => runner.start()); + }); + group('listening to stdin', () { late Stdin stdin; late StreamController> stdinController; @@ -228,6 +279,7 @@ void main() { devServerRunnerBuilder: ({ required logger, required port, + required address, required devServerBundleGenerator, required dartVmServicePort, required workingDirectory, diff --git a/packages/dart_frog_cli/test/src/daemon/domain/dev_server_domain_test.dart b/packages/dart_frog_cli/test/src/daemon/domain/dev_server_domain_test.dart index f76fb562d..70f3854f5 100644 --- a/packages/dart_frog_cli/test/src/daemon/domain/dev_server_domain_test.dart +++ b/packages/dart_frog_cli/test/src/daemon/domain/dev_server_domain_test.dart @@ -38,6 +38,7 @@ void main() { devServerRunnerBuilder: ({ required logger, required port, + required address, required devServerBundleGenerator, required dartVmServicePort, required workingDirectory, @@ -60,6 +61,7 @@ void main() { test('starts application', () async { late Logger passedLogger; late String passedPort; + late InternetAddress? passedAddress; late MasonGenerator passedDevServerBundleGenerator; late String passedDartVmServicePort; late Directory passedWorkingDirectory; @@ -70,6 +72,7 @@ void main() { devServerRunnerBuilder: ({ required logger, required port, + required address, required devServerBundleGenerator, required dartVmServicePort, required workingDirectory, @@ -77,6 +80,7 @@ void main() { }) { passedLogger = logger; passedPort = port; + passedAddress = address; passedDevServerBundleGenerator = devServerBundleGenerator; passedDartVmServicePort = dartVmServicePort; passedWorkingDirectory = workingDirectory; @@ -93,6 +97,7 @@ void main() { params: { 'workingDirectory': '/', 'port': 3000, + 'hostname': '192.168.1.2', 'dartVmServicePort': 3001, }, ), @@ -107,6 +112,7 @@ void main() { expect(passedLogger, isA()); expect(passedPort, equals('3000')); + expect(passedAddress, InternetAddress.tryParse('192.168.1.2')); expect(passedDevServerBundleGenerator, same(generator)); expect(passedDartVmServicePort, equals('3001')); expect(passedWorkingDirectory.path, equals('/')); @@ -261,6 +267,33 @@ void main() { ), ); }); + + test('hostname', () async { + expect( + await domain.handleRequest( + const DaemonRequest( + id: '12', + domain: 'dev_server', + method: 'start', + params: { + 'workingDirectory': '/', + 'port': 4040, + 'dartVmServicePort': 4041, + 'hostname': 'lol', + }, + ), + ), + equals( + const DaemonResponse.error( + id: '12', + error: { + 'message': 'Malformed message, invalid hostname "lol": ' + 'must be a valid IPv4 or IPv6 address.', + }, + ), + ), + ); + }); }); test('on dev server throw', () async { @@ -593,6 +626,7 @@ void main() { devServerRunnerBuilder: ({ required logger, required port, + required address, required devServerBundleGenerator, required dartVmServicePort, required workingDirectory, diff --git a/packages/dart_frog_cli/test/src/dev_server_runner/dev_server_runner_test.dart b/packages/dart_frog_cli/test/src/dev_server_runner/dev_server_runner_test.dart index c11555c9d..fa8cbdf50 100644 --- a/packages/dart_frog_cli/test/src/dev_server_runner/dev_server_runner_test.dart +++ b/packages/dart_frog_cli/test/src/dev_server_runner/dev_server_runner_test.dart @@ -73,6 +73,7 @@ void main() { devServerRunner = DevServerRunner( logger: logger, port: port, + address: null, devServerBundleGenerator: generator, dartVmServicePort: dartVmServicePort, workingDirectory: Directory.current, @@ -118,6 +119,7 @@ void main() { DevServerRunner( logger: Logger(), port: '8080', + address: null, devServerBundleGenerator: _MockMasonGenerator(), dartVmServicePort: '8081', workingDirectory: Directory.current, @@ -146,6 +148,12 @@ void main() { onVarsChanged: any(named: 'onVarsChanged'), ), ).called(1); + + verify(() { + progress.complete('Running on ${link( + uri: Uri.parse('http://localhost:8080'), + )}'); + }).called(1); }); test('throws when server process is already running', () async { @@ -186,6 +194,7 @@ void main() { devServerRunner = DevServerRunner( logger: logger, port: '4242', + address: null, devServerBundleGenerator: generator, dartVmServicePort: '4343', workingDirectory: Directory.current, @@ -221,6 +230,65 @@ void main() { onVarsChanged: any(named: 'onVarsChanged'), ), ).called(1); + + verify(() { + progress.complete('Running on ${link( + uri: Uri.parse('http://localhost:4242'), + )}'); + }).called(1); + }); + + test('custom address', () async { + late List receivedArgs; + + devServerRunner = DevServerRunner( + logger: logger, + port: '4242', + address: InternetAddress.tryParse('192.162.1.2'), + devServerBundleGenerator: generator, + dartVmServicePort: '4343', + workingDirectory: Directory.current, + directoryWatcher: (_) => directoryWatcher, + generatorTarget: (_, {createFile, logger}) => generatorTarget, + isWindows: isWindows, + startProcess: ( + String executable, + List arguments, { + bool runInShell = false, + }) async { + receivedArgs = arguments; + return process; + }, + sigint: sigint, + runProcess: (_, __) async => processResult, + ); + + await expectLater(devServerRunner.start(), completes); + + expect(devServerRunner.isWatching, isTrue); + expect(devServerRunner.isServerRunning, isTrue); + expect(devServerRunner.isCompleted, isFalse); + + expect( + receivedArgs, + contains('--enable-vm-service=4343'), + ); + verify( + () => generatorHooks.preGen( + vars: { + 'port': '4242', + 'host': '192.162.1.2', + }, + workingDirectory: any(named: 'workingDirectory'), + onVarsChanged: any(named: 'onVarsChanged'), + ), + ).called(1); + + verify(() { + progress.complete('Running on ${link( + uri: Uri.parse('http://192.162.1.2:4242'), + )}'); + }).called(1); }); test( @@ -238,6 +306,7 @@ void main() { devServerRunner = DevServerRunner( logger: logger, port: port, + address: null, devServerBundleGenerator: generator, dartVmServicePort: dartVmServicePort, workingDirectory: Directory.current, @@ -389,6 +458,7 @@ void main() { devServerRunner = DevServerRunner( logger: logger, port: '4242', + address: null, devServerBundleGenerator: generator, dartVmServicePort: '4343', workingDirectory: Directory.current, @@ -720,6 +790,7 @@ runs codegen with debounce when changes are made to the public or routes directo devServerRunner = DevServerRunner( logger: logger, port: port, + address: null, devServerBundleGenerator: generator, dartVmServicePort: dartVmServicePort, workingDirectory: Directory.current, @@ -821,6 +892,7 @@ runs codegen with debounce when changes are made to the public or routes directo devServerRunner = DevServerRunner( logger: logger, port: port, + address: null, devServerBundleGenerator: generator, dartVmServicePort: dartVmServicePort, workingDirectory: Directory.current, @@ -861,6 +933,7 @@ lib/my_model.g.dart:53:20: Warning: Operand of null-aware operation '!' has type devServerRunner = DevServerRunner( logger: logger, port: port, + address: null, devServerBundleGenerator: generator, dartVmServicePort: dartVmServicePort, workingDirectory: Directory.current, @@ -902,6 +975,7 @@ Could not start the VM service: localhost:8181 is already in use.'''; devServerRunner = DevServerRunner( logger: logger, port: port, + address: null, devServerBundleGenerator: generator, dartVmServicePort: dartVmServicePort, workingDirectory: Directory.current,