diff --git a/packages/flutter/test/material/radio_list_tile_test.dart b/packages/flutter/test/material/radio_list_tile_test.dart index 3cc02199f93c0..ccf5f951d3397 100644 --- a/packages/flutter/test/material/radio_list_tile_test.dart +++ b/packages/flutter/test/material/radio_list_tile_test.dart @@ -2285,7 +2285,9 @@ void main() { const BorderSide side = BorderSide(color: Colors.red, width: 3.0); await tester.pumpWidget( const MaterialApp( - home: Material(child: Center(child: RadioListTile(value: true, radioSide: side))), + home: Material( + child: Center(child: RadioListTile(value: true, radioSide: side)), + ), ), ); diff --git a/packages/flutter/test/widgets/tap_region_test.dart b/packages/flutter/test/widgets/tap_region_test.dart index 87cf705a60f19..406ee7552de3f 100644 --- a/packages/flutter/test/widgets/tap_region_test.dart +++ b/packages/flutter/test/widgets/tap_region_test.dart @@ -1201,18 +1201,17 @@ void main() { Navigator.push( tester.element(find.byType(GestureDetector)), MaterialPageRoute( - builder: - (BuildContext context) => Scaffold( - body: Center( - child: ElevatedButton( - key: buttonKey, - onPressed: () { - buttonTapped = true; - }, - child: const Text('Test Button'), - ), - ), + builder: (BuildContext context) => Scaffold( + body: Center( + child: ElevatedButton( + key: buttonKey, + onPressed: () { + buttonTapped = true; + }, + child: const Text('Test Button'), ), + ), + ), ), ); }, diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index 576e7b362d6d8..546262c427997 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -26,6 +26,7 @@ import '../drive/drive_service.dart'; import '../drive/web_driver_service.dart' show Browser; import '../globals.dart' as globals; import '../ios/devices.dart'; +import '../isolated/devfs_config.dart'; import '../resident_runner.dart'; import '../runner/flutter_command.dart' show FlutterCommandCategory, FlutterCommandResult, FlutterOptions; @@ -304,7 +305,14 @@ class DriveCommand extends RunCommandBase { _logger.printError('Screenshot not supported for ${device.displayName}.'); } - final bool web = device is WebServerDevice || device is ChromiumDevice; + final DevConfig? devConfig = (device is WebServerDevice || device is ChromiumDevice) + ? await loadDevConfig( + hostname: stringArg('web-hostname'), + port: stringArg('web-port'), + tlsCertPath: stringArg('web-tls-cert-path'), + tlsCertKeyPath: stringArg('web-tls-cert-key-path'), + ) + : null; _flutterDriverFactory ??= FlutterDriverFactory( applicationPackageFactory: ApplicationPackageFactory.instance!, logger: _logger, @@ -322,9 +330,11 @@ class DriveCommand extends RunCommandBase { logger: _logger, throwOnError: false, ); - final DriverService driverService = _flutterDriverFactory!.createDriverService(web); + final DriverService driverService = _flutterDriverFactory!.createDriverService( + devConfig != null, + ); final BuildInfo buildInfo = await getBuildInfo(); - final DebuggingOptions debuggingOptions = await createDebuggingOptions(web); + final DebuggingOptions debuggingOptions = await createDebuggingOptions(devConfig: devConfig); final File? applicationBinary = applicationBinaryPath == null ? null : _fileSystem.file(applicationBinaryPath); @@ -342,7 +352,7 @@ class DriveCommand extends RunCommandBase { mainPath: targetFile, platformArgs: { if (traceStartup) 'trace-startup': traceStartup, - if (web) '--no-launch-chrome': true, + if (devConfig != null) '--no-launch-chrome': true, }, ); } else { diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 458ed3e1cee8e..72d1e66c317d5 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -17,6 +17,7 @@ import '../device.dart'; import '../features.dart'; import '../globals.dart' as globals; import '../ios/devices.dart'; +import '../isolated/devfs_config.dart'; import '../project.dart'; import '../resident_runner.dart'; import '../run_cold.dart'; @@ -269,7 +270,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment /// Create a debugging options instance for the current `run` or `drive` invocation. @visibleForTesting @protected - Future createDebuggingOptions(bool webMode) async { + Future createDebuggingOptions({DevConfig? devConfig}) async { final BuildInfo buildInfo = await getBuildInfo(); final int? webBrowserDebugPort = featureFlags.isWebEnabled && argResults!.wasParsed('web-browser-debug-port') @@ -279,18 +280,10 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment ? stringsArg(FlutterOptions.kWebBrowserFlag) : const []; - final Map webHeaders = featureFlags.isWebEnabled - ? extractWebHeaders() - : const {}; - if (buildInfo.mode.isRelease) { return DebuggingOptions.disabled( buildInfo, dartEntrypointArgs: stringsArg('dart-entrypoint-args'), - hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '', - port: featureFlags.isWebEnabled ? stringArg('web-port') : '', - tlsCertPath: featureFlags.isWebEnabled ? stringArg('web-tls-cert-path') : null, - tlsCertKeyPath: featureFlags.isWebEnabled ? stringArg('web-tls-cert-key-path') : null, webUseSseForDebugProxy: featureFlags.isWebEnabled && stringArg('web-server-debug-protocol') == 'sse', webUseSseForDebugBackend: @@ -302,7 +295,6 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment webRunHeadless: featureFlags.isWebEnabled && boolArg('web-run-headless'), webBrowserDebugPort: webBrowserDebugPort, webBrowserFlags: webBrowserFlags, - webHeaders: webHeaders, webRenderer: webRenderer, webUseWasm: useWasm, enableImpeller: enableImpeller, @@ -313,6 +305,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment enableEmbedderApi: enableEmbedderApi, usingCISystem: usingCISystem, debugLogsDirectoryPath: debugLogsDirectoryPath, + devConfig: devConfig, ); } else { return DebuggingOptions.enabled( @@ -344,10 +337,6 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment ddsPort: ddsPort, devToolsServerAddress: devToolsServerAddress, verboseSystemLogs: boolArg('verbose-system-logs'), - hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '', - port: featureFlags.isWebEnabled ? stringArg('web-port') : '', - tlsCertPath: featureFlags.isWebEnabled ? stringArg('web-tls-cert-path') : null, - tlsCertKeyPath: featureFlags.isWebEnabled ? stringArg('web-tls-cert-key-path') : null, webUseSseForDebugProxy: featureFlags.isWebEnabled && stringArg('web-server-debug-protocol') == 'sse', webUseSseForDebugBackend: @@ -362,7 +351,6 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment webEnableExpressionEvaluation: featureFlags.isWebEnabled && boolArg('web-enable-expression-evaluation'), webLaunchUrl: featureFlags.isWebEnabled ? stringArg('web-launch-url') : null, - webHeaders: webHeaders, webRenderer: webRenderer, webUseWasm: useWasm, vmserviceOutFile: stringArg('vmservice-out-file'), @@ -382,6 +370,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment enableDevTools: boolArg(FlutterCommand.kEnableDevTools), ipv6: boolArg(FlutterCommand.ipv6Flag), printDtd: boolArg(FlutterGlobalOptions.kPrintDtd, global: true), + devConfig: devConfig, ); } } @@ -493,7 +482,7 @@ class RunCommand extends RunCommandBase { String get category => FlutterCommandCategory.project; List? devices; - var webMode = false; + late final DevConfig? _devConfig; String? get userIdentifier => stringArg(FlutterOptions.kDeviceUser); @@ -663,12 +652,21 @@ class RunCommand extends RunCommandBase { // Only support "web mode" with a single web device due to resident runner // refactoring required otherwise. - webMode = - featureFlags.isWebEnabled && + if (featureFlags.isWebEnabled && devices!.length == 1 && - await devices!.single.targetPlatform == TargetPlatform.web_javascript; + await devices!.single.targetPlatform == TargetPlatform.web_javascript) { + _devConfig = await loadDevConfig( + hostname: stringArg('web-hostname'), + port: stringArg('web-port'), + tlsCertPath: stringArg('web-tls-cert-path'), + tlsCertKeyPath: stringArg('web-tls-cert-key-path'), + headers: extractWebHeaders(), + ); + } else { + _devConfig = null; + } - if (useWasm && !webMode) { + if (useWasm && _devConfig == null) { throwToolExit('--wasm is only supported on the web platform'); } @@ -696,11 +694,13 @@ class RunCommand extends RunCommandBase { required String? applicationBinaryPath, required FlutterProject flutterProject, }) async { - if (hotMode && !webMode) { + final DebuggingOptions debuggingOptions = await createDebuggingOptions(devConfig: _devConfig); + + if (hotMode && _devConfig == null) { return HotRunner( flutterDevices, target: targetFile, - debuggingOptions: await createDebuggingOptions(webMode), + debuggingOptions: debuggingOptions, benchmarkMode: boolArg('benchmark'), applicationBinary: applicationBinaryPath == null ? null @@ -711,12 +711,12 @@ class RunCommand extends RunCommandBase { analytics: globals.analytics, nativeAssetsYamlFile: stringArg(FlutterOptions.kNativeAssetsYamlFile), ); - } else if (webMode) { + } else if (_devConfig != null) { return webRunnerFactory!.createWebRunner( flutterDevices.single, target: targetFile, flutterProject: flutterProject, - debuggingOptions: await createDebuggingOptions(webMode), + debuggingOptions: debuggingOptions, stayResident: stayResident, fileSystem: globals.fs, analytics: globals.analytics, @@ -730,7 +730,7 @@ class RunCommand extends RunCommandBase { return ColdRunner( flutterDevices, target: targetFile, - debuggingOptions: await createDebuggingOptions(webMode), + debuggingOptions: debuggingOptions, traceStartup: traceStartup, awaitFirstFrameWhenTracing: awaitFirstFrameWhenTracing, applicationBinary: applicationBinaryPath == null @@ -759,13 +759,15 @@ class RunCommand extends RunCommandBase { } final Daemon daemon = createMachineDaemon(); late AppInstance app; + + final DebuggingOptions debuggingOptions = await createDebuggingOptions(devConfig: _devConfig); try { app = await daemon.appDomain.startApp( devices!.first, globals.fs.currentDirectory.path, targetFile, route, - await createDebuggingOptions(webMode), + debuggingOptions, hotMode, applicationBinary: applicationBinaryPath == null ? null diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index a7320df0bfb9e..48222eabeb88d 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -17,6 +17,7 @@ import 'build_info.dart'; import 'devfs.dart'; import 'device_port_forwarder.dart'; import 'device_vm_service_discovery_for_attach.dart'; +import 'isolated/devfs_config.dart'; import 'project.dart'; import 'vmservice.dart'; import 'web/compile.dart'; @@ -948,10 +949,6 @@ class DebuggingOptions { this.deviceVmServicePort, this.ddsPort, this.devToolsServerAddress, - this.hostname, - this.port, - this.tlsCertPath, - this.tlsCertKeyPath, this.webEnableExposeUrl, this.webUseSseForDebugProxy = true, this.webUseSseForDebugBackend = true, @@ -960,7 +957,6 @@ class DebuggingOptions { this.webBrowserDebugPort, this.webBrowserFlags = const [], this.webEnableExpressionEvaluation = false, - this.webHeaders = const {}, this.webLaunchUrl, WebRendererMode? webRenderer, this.webUseWasm = false, @@ -979,16 +975,13 @@ class DebuggingOptions { this.ipv6 = false, this.google3WorkspaceRoot, this.printDtd = false, + this.devConfig = const DevConfig(), }) : debuggingEnabled = true, webRenderer = webRenderer ?? WebRendererMode.getDefault(useWasm: webUseWasm); DebuggingOptions.disabled( this.buildInfo, { this.dartEntrypointArgs = const [], - this.port, - this.hostname, - this.tlsCertPath, - this.tlsCertKeyPath, this.webEnableExposeUrl, this.webUseSseForDebugProxy = true, this.webUseSseForDebugBackend = true, @@ -997,7 +990,6 @@ class DebuggingOptions { this.webBrowserDebugPort, this.webBrowserFlags = const [], this.webLaunchUrl, - this.webHeaders = const {}, WebRendererMode? webRenderer, this.webUseWasm = false, this.traceAllowlist, @@ -1009,6 +1001,7 @@ class DebuggingOptions { this.enableEmbedderApi = false, this.usingCISystem = false, this.debugLogsDirectoryPath, + this.devConfig = const DevConfig(), }) : debuggingEnabled = false, useTestFonts = false, startPaused = false, @@ -1067,10 +1060,6 @@ class DebuggingOptions { required this.disablePortPublication, required this.ddsPort, required this.devToolsServerAddress, - required this.port, - required this.hostname, - required this.tlsCertPath, - required this.tlsCertKeyPath, required this.webEnableExposeUrl, required this.webUseSseForDebugProxy, required this.webUseSseForDebugBackend, @@ -1079,7 +1068,6 @@ class DebuggingOptions { required this.webBrowserDebugPort, required this.webBrowserFlags, required this.webEnableExpressionEvaluation, - required this.webHeaders, required this.webLaunchUrl, required this.webRenderer, required this.webUseWasm, @@ -1098,6 +1086,8 @@ class DebuggingOptions { required this.ipv6, required this.google3WorkspaceRoot, required this.printDtd, + // ignore: unused_element_parameter + this.devConfig, }); final bool debuggingEnabled; @@ -1126,10 +1116,6 @@ class DebuggingOptions { final bool disablePortPublication; final int? ddsPort; final Uri? devToolsServerAddress; - final String? port; - final String? hostname; - final String? tlsCertPath; - final String? tlsCertKeyPath; final bool? webEnableExposeUrl; final bool webUseSseForDebugProxy; final bool webUseSseForDebugBackend; @@ -1145,6 +1131,7 @@ class DebuggingOptions { final bool ipv6; final String? google3WorkspaceRoot; final bool printDtd; + final DevConfig? devConfig; /// Whether the tool should try to uninstall a previously installed version of the app. /// @@ -1170,9 +1157,6 @@ class DebuggingOptions { /// Allow developers to customize the browser's launch URL final String? webLaunchUrl; - /// Allow developers to add custom headers to web server - final Map webHeaders; - /// Which web renderer to use for the debugging session final WebRendererMode webRenderer; @@ -1268,10 +1252,6 @@ class DebuggingOptions { 'disablePortPublication': disablePortPublication, 'ddsPort': ddsPort, 'devToolsServerAddress': devToolsServerAddress.toString(), - 'port': port, - 'hostname': hostname, - 'tlsCertPath': tlsCertPath, - 'tlsCertKeyPath': tlsCertKeyPath, 'webEnableExposeUrl': webEnableExposeUrl, 'webUseSseForDebugProxy': webUseSseForDebugProxy, 'webUseSseForDebugBackend': webUseSseForDebugBackend, @@ -1281,7 +1261,6 @@ class DebuggingOptions { 'webBrowserFlags': webBrowserFlags, 'webEnableExpressionEvaluation': webEnableExpressionEvaluation, 'webLaunchUrl': webLaunchUrl, - 'webHeaders': webHeaders, 'webRenderer': webRenderer.name, 'webUseWasm': webUseWasm, 'vmserviceOutFile': vmserviceOutFile, @@ -1337,10 +1316,6 @@ class DebuggingOptions { devToolsServerAddress: json['devToolsServerAddress'] != null ? Uri.parse(json['devToolsServerAddress']! as String) : null, - port: json['port'] as String?, - hostname: json['hostname'] as String?, - tlsCertPath: json['tlsCertPath'] as String?, - tlsCertKeyPath: json['tlsCertKeyPath'] as String?, webEnableExposeUrl: json['webEnableExposeUrl'] as bool?, webUseSseForDebugProxy: json['webUseSseForDebugProxy']! as bool, webUseSseForDebugBackend: json['webUseSseForDebugBackend']! as bool, @@ -1349,7 +1324,6 @@ class DebuggingOptions { webBrowserDebugPort: json['webBrowserDebugPort'] as int?, webBrowserFlags: (json['webBrowserFlags']! as List).cast(), webEnableExpressionEvaluation: json['webEnableExpressionEvaluation']! as bool, - webHeaders: (json['webHeaders']! as Map).cast(), webLaunchUrl: json['webLaunchUrl'] as String?, webRenderer: WebRendererMode.values.byName(json['webRenderer']! as String), webUseWasm: json['webUseWasm']! as bool, diff --git a/packages/flutter_tools/lib/src/drive/web_driver_service.dart b/packages/flutter_tools/lib/src/drive/web_driver_service.dart index 27fde99423dd0..cd8eb3c27290e 100644 --- a/packages/flutter_tools/lib/src/drive/web_driver_service.dart +++ b/packages/flutter_tools/lib/src/drive/web_driver_service.dart @@ -85,20 +85,16 @@ class WebDriverService extends DriverService { debuggingOptions: buildInfo.isRelease ? DebuggingOptions.disabled( buildInfo, - port: debuggingOptions.port, - hostname: debuggingOptions.hostname, + devConfig: debuggingOptions.devConfig, webRenderer: debuggingOptions.webRenderer, webUseWasm: debuggingOptions.webUseWasm, - webHeaders: debuggingOptions.webHeaders, ) : DebuggingOptions.enabled( buildInfo, - port: debuggingOptions.port, - hostname: debuggingOptions.hostname, + devConfig: debuggingOptions.devConfig, disablePortPublication: debuggingOptions.disablePortPublication, webRenderer: debuggingOptions.webRenderer, webUseWasm: debuggingOptions.webUseWasm, - webHeaders: debuggingOptions.webHeaders, ), stayResident: true, flutterProject: FlutterProject.current(), diff --git a/packages/flutter_tools/lib/src/isolated/devfs_config.dart b/packages/flutter_tools/lib/src/isolated/devfs_config.dart new file mode 100644 index 0000000000000..592e7f66a04a5 --- /dev/null +++ b/packages/flutter_tools/lib/src/isolated/devfs_config.dart @@ -0,0 +1,261 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io' as io; + +import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +import '../base/common.dart'; +import '../globals.dart' as globals; +import 'devfs_proxy.dart'; + +const devConfigFilePath = 'web_dev_config.yaml'; + +@immutable +class DevConfig { + const DevConfig({ + this.headers = const {}, + this.host = 'any', + this.port, + this.https, + this.proxy = const [], + }); + + factory DevConfig.fromYaml(YamlMap yaml) { + final headers = {}; + if (yaml['headers'] != null) { + if (yaml['headers'] is! YamlList) { + throwToolExit('Headers must be a List of maps. Found ${yaml['headers'].runtimeType}'); + } + final headersList = yaml['headers'] as YamlList; + for (final dynamic item in headersList) { + if (item is! YamlMap) { + throwToolExit( + 'Each header entry must be a map with "name" and "value" keys. Found ${item.runtimeType}', + ); + } + final YamlMap headerMap = item; + if (!headerMap.containsKey('name') || !headerMap.containsKey('value')) { + throwToolExit('Each header entry must contain "name" and "value" keys.'); + } + final dynamic name = headerMap['name']; + final dynamic value = headerMap['value']; + + if (name is! String || value is! String) { + throwToolExit( + 'Header "name" and "value" must be strings. Found name: ${name.runtimeType}, value: ${value.runtimeType}', + ); + } + headers[name] = value; + } + } + if (yaml['host'] is! String && yaml['host'] != null) { + throwToolExit('Host must be a String. Found ${yaml['host'].runtimeType}'); + } + if (yaml['port'] is! int && yaml['port'] != null) { + throwToolExit('Port must be an int. Found ${yaml['port'].runtimeType}'); + } + if (yaml['https'] is! YamlMap && yaml['https'] != null) { + throwToolExit('Https must be a Map. Found ${yaml['https'].runtimeType}'); + } + + final proxyRules = []; + if (yaml['proxy'] != null) { + if (yaml['proxy'] is! YamlList) { + throwToolExit('Proxy must be a list. Found ${yaml['proxy'].runtimeType}'); + } + final proxyList = yaml['proxy'] as YamlList; + for (final dynamic item in proxyList) { + if (item is YamlMap) { + final ProxyRule? rule = ProxyRule.fromYaml(item); + if (rule != null) { + proxyRules.add(rule); + } + } + } + } + + return DevConfig( + headers: headers, + host: yaml['host'] as String?, + port: yaml['port'] as int?, + https: yaml['https'] == null ? null : HttpsConfig.fromYaml(yaml['https'] as YamlMap), + proxy: proxyRules, + ); + } + + final Map headers; + final String? host; + final int? port; + final HttpsConfig? https; + final List proxy; + + @override + String toString() { + return ''' + DevConfig: + headers: $headers + host: $host + port: $port + https: $https + proxy: $proxy'''; + } +} + +@immutable +class HttpsConfig { + const HttpsConfig({required this.certPath, required this.certKeyPath}); + + factory HttpsConfig.fromYaml(YamlMap yaml) { + if (yaml['cert-path'] is! String && yaml['cert-path'] != null) { + throwToolExit('Https cert-path must be a String. Found ${yaml['cert-path'].runtimeType}'); + } + if (yaml['cert-key-path'] is! String && yaml['cert-key-path'] != null) { + throwToolExit( + 'Https cert-key-path must be a String. Found ${yaml['cert-key-path'].runtimeType}', + ); + } + return HttpsConfig( + certPath: yaml['cert-path'] as String?, + certKeyPath: yaml['cert-key-path'] as String?, + ); + } + + final String? certPath; + final String? certKeyPath; + + @override + String toString() { + return ''' + HttpsConfig: + certPath: $certPath + certKeyPath: $certKeyPath'''; + } +} + +T? _getOverriddenValue(String filedName, T? fileValue, T? cliValue) { + if (cliValue != null) { + if (fileValue != null && cliValue != fileValue) { + globals.printStatus( + 'Overriding $filedName from $devConfigFilePath ($fileValue) with command-line argument ($cliValue)', + ); + } + return cliValue; + } + return fileValue; +} + +Future loadDevConfig({ + String? hostname, + String? port, + String? tlsCertPath, + String? tlsCertKeyPath, + Map? headers, + int? debugPort, + List? browserFlags, +}) async { + final io.File devConfigFile = globals.fs.file(devConfigFilePath); + var fileConfig = const DevConfig(); + + if (!devConfigFile.existsSync()) { + globals.printStatus('No $devConfigFilePath found'); + } else { + try { + final String devConfigContent = await devConfigFile.readAsString(); + final YamlDocument yamlDoc = loadYamlDocument(devConfigContent); + final YamlNode contents = yamlDoc.contents; + if (contents is! YamlMap) { + throw YamlException( + '$devConfigFilePath file found, but it must be a YAML map (e.g., "server:"). Found a ${contents.runtimeType} instead.', + contents.span, + ); + } + + if (!contents.containsKey('server') || contents['server'] is! YamlMap) { + final SourceSpan span = (contents.containsKey('server') && contents['server'] is YamlNode) + ? (contents['server'] as YamlNode).span + : contents.span; + throw YamlException( + '"$devConfigFilePath" file found, but the "server" key is missing or malformed. It must be a YAML map.', + span, + ); + } + + final serverYaml = contents['server'] as YamlMap; + fileConfig = DevConfig.fromYaml(serverYaml); + globals.printStatus('\nLoaded configuration from $devConfigFilePath'); + globals.printTrace(fileConfig.toString()); + } on YamlException catch (e) { + globals.printError('Error: Failed to parse $devConfigFilePath: ${e.message} ${e.span}'); + rethrow; + } on Exception catch (e) { + globals.printError('An unexpected error occurred while reading $devConfigFilePath: $e'); + globals.printStatus( + 'Reverting to default flutter_tools web server configuration due to unexpected error.', + ); + } + } + + final String finalHost = + _getOverriddenValue('host', fileConfig.host, hostname) ?? 'localhost'; + final int? finalPort = _getOverriddenValue( + 'port', + fileConfig.port, + port != null ? int.tryParse(port) : null, + ); + final String? finalCertPath = _getOverriddenValue( + 'TLS cert path', + fileConfig.https?.certPath, + tlsCertPath, + ); + final String? finalCertKeyPath = _getOverriddenValue( + 'TLS cert key path', + fileConfig.https?.certKeyPath, + tlsCertKeyPath, + ); + HttpsConfig? finalHttpsConfig; + if (finalCertPath != null || finalCertKeyPath != null || fileConfig.https != null) { + finalHttpsConfig = HttpsConfig(certPath: finalCertPath, certKeyPath: finalCertKeyPath); + } else { + finalHttpsConfig = null; + } + final finalHeaders = {}; + finalHeaders.addAll(fileConfig.headers); + if (headers != null && headers.isNotEmpty) { + for (final MapEntry entry in headers.entries) { + if (fileConfig.headers.containsKey(entry.key)) { + globals.printStatus( + 'Overriding headers "${entry.key}" from $devConfigFilePath ("${fileConfig.headers[entry.key]}") with command-line argument("${entry.value}").', + ); + } else { + globals.printStatus('Adding header "${entry.key}" from command-line arguments.'); + } + } + finalHeaders.addAll(headers); + } + + return DevConfig( + host: finalHost, + port: finalPort, + https: finalHttpsConfig, + headers: finalHeaders, + proxy: fileConfig.proxy, + ); +} + +Future resolvePort(int? port) async { + if (port == null) { + return globals.os.findFreePort(); + } + if (port < 0 || port > 65535) { + throwToolExit(''' +Invalid port: $port +Please provide a valid TCP port (an integer between 0 and 65535, inclusive). +'''); + } + return port; +} diff --git a/packages/flutter_tools/lib/src/isolated/devfs_proxy.dart b/packages/flutter_tools/lib/src/isolated/devfs_proxy.dart new file mode 100644 index 0000000000000..c8736ad6a22c1 --- /dev/null +++ b/packages/flutter_tools/lib/src/isolated/devfs_proxy.dart @@ -0,0 +1,164 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf_proxy/shelf_proxy.dart'; +import 'package:yaml/yaml.dart'; +import '/src/base/logger.dart'; +import '../globals.dart' as globals; + +abstract class ProxyRule { + ProxyRule({required this.target}); + + final String target; + String replace(String path); + bool matches(String path); + + static ProxyRule? fromYaml(YamlMap yaml, {Logger? logger}) { + final target = yaml['target'] as String?; + final source = yaml['source'] as String?; + final regex = yaml['regex'] as String?; + final replace = yaml['replace'] as String?; + final Logger effectiveLogger = logger ?? globals.logger; + + if (target == null) { + final String? path = source ?? regex; + effectiveLogger.printError("Invalid 'target' for path: $path. 'target' cannot be null"); + return null; + } + RegExp? proxyPattern; + if (source != null && source.isNotEmpty) { + return SourceProxyRule(source: source, target: target, replacement: replace?.trim()); + } else if (regex != null && regex.isNotEmpty) { + try { + proxyPattern = RegExp(regex.trim()); + } on FormatException catch (e) { + proxyPattern = RegExp(RegExp.escape(regex)); + effectiveLogger.printWarning( + "Invalid regex pattern in replace 'regex': '$regex'. Treating $regex as string. Error: $e", + ); + } + return RegexProxyRule(pattern: proxyPattern, target: target, replacement: replace?.trim()); + } else { + effectiveLogger.printError("'source' or 'regex' field must be provided"); + return null; + } + } +} + +class RegexProxyRule extends ProxyRule { + RegexProxyRule({required this.pattern, required super.target, this.replacement}); + + final RegExp pattern; + final String? replacement; + + @override + bool matches(String path) { + return pattern.hasMatch(path); + } + + @override + String replace(String path) { + if (replacement == null) { + return path; + } + return path.replaceAllMapped(pattern, (Match match) { + String result = replacement!; + + for (var i = 0; i <= match.groupCount; i++) { + result = result.replaceAll('\$$i', match.group(i) ?? ''); + } + return result; + }); + } + + @override + String toString() { + return '{pattern: ${pattern.pattern}, target: $target, replacement: ${replacement ?? 'null'}}'; + } +} + +class SourceProxyRule extends ProxyRule { + SourceProxyRule({required this.source, required super.target, this.replacement}); + final String source; + final String? replacement; + + @override + bool matches(String path) { + return path.startsWith(source); + } + + @override + String replace(String path) { + if (replacement == null) { + return path; + } + return path.replaceFirst(source, replacement!); + } + + @override + String toString() { + return '{source: $source, target: $target, replacement: ${replacement ?? 'null'}}'; + } +} + +shelf.Request proxyRequest(shelf.Request originalRequest, Uri finalTargetUrl) { + return shelf.Request( + originalRequest.method, + finalTargetUrl, + headers: originalRequest.headers, + body: originalRequest.read(), + context: originalRequest.context, + ); +} + +String _normalizePath(String path) { + if (!path.startsWith('/')) { + path = '/$path'; + } + return path; +} + +shelf.Middleware proxyMiddleware(List effectiveProxy) { + return (shelf.Handler innerHandler) { + return (shelf.Request request) async { + final String requestPath = _normalizePath(request.url.path); + for (final rule in effectiveProxy) { + if (rule.matches(requestPath)) { + final Uri targetBaseUri = Uri.parse(rule.target); + final String rewrittenRequest = rule.replace(requestPath); + final Uri finalTargetUrl = targetBaseUri.resolve(rewrittenRequest); + try { + final shelf.Request proxyBackendRequest = proxyRequest(request, finalTargetUrl); + final shelf.Response proxyResponse = await proxyHandler(targetBaseUri)( + proxyBackendRequest, + ); + final internalRequest = proxyResponse.headers['sec-fetch-mode'] == 'no-cors'; + if (!internalRequest) { + globals.logger.printStatus( + '[PROXY] Matched "$requestPath". Requesting "$finalTargetUrl"', + ); + globals.logger.printTrace('[PROXY] Matched with proxy rule: $rule'); + } + if (proxyResponse.statusCode == 404) { + if (!internalRequest) { + globals.printTrace('"$finalTargetUrl" responded with status 404'); + } + return innerHandler(request); + } + return proxyResponse; + } on Exception catch (e) { + globals.logger.printError( + 'Proxy error for $finalTargetUrl: $e. Allowing fall-through.', + ); + + return innerHandler(request); + } + } + } + + return innerHandler(request); + }; + }; +} diff --git a/packages/flutter_tools/lib/src/isolated/devfs_web.dart b/packages/flutter_tools/lib/src/isolated/devfs_web.dart index cebd4a83c489e..6d11e427c4242 100644 --- a/packages/flutter_tools/lib/src/isolated/devfs_web.dart +++ b/packages/flutter_tools/lib/src/isolated/devfs_web.dart @@ -30,7 +30,7 @@ import '../web/bootstrap.dart'; import '../web/chrome.dart'; import '../web/compile.dart'; import '../web_template.dart'; - +import 'devfs_config.dart'; import 'web_asset_server.dart'; const kLuciEnvName = 'LUCI_CONTEXT'; @@ -60,10 +60,6 @@ class WebDevFS implements DevFS { /// [testMode] is true, do not actually initialize dwds or the shelf static /// server. WebDevFS({ - required this.hostname, - required int port, - required this.tlsCertPath, - required this.tlsCertKeyPath, required this.packagesFilePath, required this.urlTunneller, required this.useSseForDebugProxy, @@ -74,11 +70,11 @@ class WebDevFS implements DevFS { required this.enableDds, required this.entrypoint, required this.expressionCompiler, - required this.extraHeaders, required this.chromiumLauncher, required this.nativeNullAssertions, required this.ddcModuleSystem, required this.canaryFeatures, + required this.devConfig, required this.webRenderer, required this.isWasm, required this.useLocalCanvasKit, @@ -88,7 +84,7 @@ class WebDevFS implements DevFS { required this.logger, required this.platform, this.testMode = false, - }) : _port = port { + }) { // TODO(srujzs): Remove this assertion when the library bundle format is // supported without canary mode. if (ddcModuleSystem) { @@ -97,7 +93,6 @@ class WebDevFS implements DevFS { } final Uri entrypoint; - final String hostname; final String packagesFilePath; final UrlTunneller? urlTunneller; final bool useSseForDebugProxy; @@ -106,19 +101,16 @@ class WebDevFS implements DevFS { final BuildInfo buildInfo; final bool enableDwds; final bool enableDds; - final Map extraHeaders; final bool testMode; final bool ddcModuleSystem; final bool canaryFeatures; final ExpressionCompiler? expressionCompiler; final ChromiumLauncher? chromiumLauncher; final bool nativeNullAssertions; - final int _port; - final String? tlsCertPath; - final String? tlsCertKeyPath; final WebRendererMode webRenderer; final bool isWasm; final bool useLocalCanvasKit; + final DevConfig devConfig; final bool useDwdsWebSocketConnection; final FileSystem fileSystem; final Logger logger; @@ -208,10 +200,6 @@ class WebDevFS implements DevFS { Future create() async { webAssetServer = await WebAssetServer.start( chromiumLauncher, - hostname, - _port, - tlsCertPath, - tlsCertKeyPath, urlTunneller, useSseForDebugProxy, useSseForDebugBackend, @@ -221,13 +209,13 @@ class WebDevFS implements DevFS { enableDds, entrypoint, expressionCompiler, - extraHeaders, webRenderer: webRenderer, isWasm: isWasm, useLocalCanvasKit: useLocalCanvasKit, testMode: testMode, ddcModuleSystem: ddcModuleSystem, canaryFeatures: canaryFeatures, + devConfig: devConfig, useDwdsWebSocketConnection: useDwdsWebSocketConnection, fileSystem: fileSystem, logger: logger, diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart index 1570d18abee08..f49e86d790561 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -41,6 +41,8 @@ import '../web/file_generators/flutter_service_worker_js.dart'; import '../web/file_generators/main_dart.dart' as main_dart; import '../web/web_device.dart'; import '../web/web_runner.dart'; + +import 'devfs_config.dart'; import 'devfs_web.dart'; import 'web_expression_compiler.dart'; @@ -274,31 +276,17 @@ class ResidentWebRunner extends ResidentRunner { try { return await asyncGuard(() async { - Future getPort() async { - if (debuggingOptions.port == null) { - return globals.os.findFreePort(); - } + final DevConfig originalDevConfig = debuggingOptions.devConfig ?? const DevConfig(); - final int? port = int.tryParse(debuggingOptions.port ?? ''); - - if (port == null) { - logger.printError(''' -Received a non-integer value for port: ${debuggingOptions.port} -A randomly-chosen available port will be used instead. -'''); - return globals.os.findFreePort(); - } - - if (port < 0 || port > 65535) { - throwToolExit(''' -Invalid port: ${debuggingOptions.port} -Please provide a valid TCP port (an integer between 0 and 65535, inclusive). - '''); - } - - return port; - } + final int resolvedPort = await resolvePort(originalDevConfig.port); + final updatedDevConfig = DevConfig( + host: originalDevConfig.host, + port: resolvedPort, + headers: originalDevConfig.headers, + https: originalDevConfig.https, + proxy: originalDevConfig.proxy, + ); final ExpressionCompiler? expressionCompiler = debuggingOptions.webEnableExpressionEvaluation ? WebExpressionCompiler(device!.generator!, fileSystem: _fileSystem) @@ -323,10 +311,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). nonWebServerConnectedDeviceIds.contains(device!.device!.id)); device!.devFS = WebDevFS( - hostname: debuggingOptions.hostname ?? 'localhost', - port: await getPort(), - tlsCertPath: debuggingOptions.tlsCertPath, - tlsCertKeyPath: debuggingOptions.tlsCertKeyPath, + devConfig: updatedDevConfig, packagesFilePath: packagesFilePath, urlTunneller: _urlTunneller, useSseForDebugProxy: debuggingOptions.webUseSseForDebugProxy, @@ -337,7 +322,6 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). enableDds: debuggingOptions.enableDds, entrypoint: _fileSystem.file(target).uri, expressionCompiler: expressionCompiler, - extraHeaders: debuggingOptions.webHeaders, chromiumLauncher: _chromiumLauncher, nativeNullAssertions: debuggingOptions.nativeNullAssertions, ddcModuleSystem: debuggingOptions.buildInfo.ddcModuleFormat == DdcModuleFormat.ddc, @@ -352,7 +336,8 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). platform: _platform, ); Uri url = await device!.devFS!.create(); - if (debuggingOptions.tlsCertKeyPath != null && debuggingOptions.tlsCertPath != null) { + if (debuggingOptions.devConfig!.https?.certKeyPath != null && + debuggingOptions.devConfig!.https?.certPath != null) { url = url.replace(scheme: 'https'); } if (debuggingOptions.buildInfo.isDebug && !debuggingOptions.webUseWasm) { diff --git a/packages/flutter_tools/lib/src/isolated/web_asset_server.dart b/packages/flutter_tools/lib/src/isolated/web_asset_server.dart index 982240a209847..4ba62ba72b57d 100644 --- a/packages/flutter_tools/lib/src/isolated/web_asset_server.dart +++ b/packages/flutter_tools/lib/src/isolated/web_asset_server.dart @@ -31,7 +31,8 @@ import '../web/compile.dart'; import '../web/memory_fs.dart'; import '../web/module_metadata.dart'; import '../web_template.dart'; - +import 'devfs_config.dart'; +import 'devfs_proxy.dart'; import 'release_asset_server.dart'; import 'web_server_utlities.dart'; @@ -134,9 +135,6 @@ class WebAssetServer implements AssetReader { /// contains a list of objects each with three fields: /// /// `src`: A string that corresponds to the file path containing a DDC library - /// bundle. To support embedded libraries, the path should include the - /// `baseUri` of the web server. - /// `module`: The name of the library bundle in `src`. /// `libraries`: An array of strings containing the libraries that were /// compiled in `src`. /// @@ -179,7 +177,7 @@ class WebAssetServer implements AssetReader { Uri? get baseUri => _baseUri; Uri? _baseUri; - /// Start the web asset server on a [hostname] and [port]. + /// Start the web asset server on a hostname and port. /// /// If [testMode] is true, do not actually initialize dwds or the shelf static /// server. @@ -188,10 +186,6 @@ class WebAssetServer implements AssetReader { /// trace. static Future start( ChromiumLauncher? chromiumLauncher, - String hostname, - int port, - String? tlsCertPath, - String? tlsCertKeyPath, UrlTunneller? urlTunneller, bool useSseForDebugProxy, bool useSseForDebugBackend, @@ -200,8 +194,8 @@ class WebAssetServer implements AssetReader { bool enableDwds, bool enableDds, Uri entrypoint, - ExpressionCompiler? expressionCompiler, - Map extraHeaders, { + ExpressionCompiler? expressionCompiler, { + required DevConfig devConfig, required WebRendererMode webRenderer, required bool isWasm, required bool useLocalCanvasKit, @@ -216,28 +210,35 @@ class WebAssetServer implements AssetReader { required Platform platform, bool shouldEnableMiddleware = true, }) async { + final String effectiveHost = devConfig.host ?? 'localhost'; + final int effectivePort = devConfig.port ?? 0; + final String? effectiveCertPath = devConfig.https?.certPath; + final String? effectiveCertKeyPath = devConfig.https?.certKeyPath; + final Map effectiveHeaders = devConfig.headers; + final List effectiveProxy = devConfig.proxy; + // TODO(srujzs): Remove this assertion when the library bundle format is // supported without canary mode. if (ddcModuleSystem) { assert(canaryFeatures); } InternetAddress address; - if (hostname == 'any') { + if (effectiveHost == 'any') { address = InternetAddress.anyIPv4; } else { - address = (await InternetAddress.lookup(hostname)).first; + address = (await InternetAddress.lookup(effectiveHost)).first; } HttpServer? httpServer; const kMaxRetries = 4; for (var i = 0; i <= kMaxRetries; i++) { try { - if (tlsCertPath != null && tlsCertKeyPath != null) { + if (effectiveCertPath != null && effectiveCertKeyPath != null) { final serverContext = SecurityContext() - ..useCertificateChain(tlsCertPath) - ..usePrivateKey(tlsCertKeyPath); - httpServer = await HttpServer.bindSecure(address, port, serverContext); + ..useCertificateChain(effectiveCertPath) + ..usePrivateKey(effectiveCertKeyPath); + httpServer = await HttpServer.bindSecure(address, effectivePort, serverContext); } else { - httpServer = await HttpServer.bind(address, port); + httpServer = await HttpServer.bind(address, effectivePort); } break; } on SocketException catch (e, s) { @@ -252,7 +253,7 @@ class WebAssetServer implements AssetReader { // Allow rendering in a iframe. httpServer!.defaultResponseHeaders.remove('x-frame-options', 'SAMEORIGIN'); - for (final MapEntry header in extraHeaders.entries) { + for (final MapEntry header in effectiveHeaders.entries) { httpServer.defaultResponseHeaders.add(header.key, header.value); } @@ -271,13 +272,13 @@ class WebAssetServer implements AssetReader { useLocalCanvasKit: useLocalCanvasKit, fileSystem: fileSystem, ); - final int selectedPort = server.selectedPort; - var url = '$hostname:$selectedPort'; - if (hostname == 'any') { + final int selectedPort = httpServer.port; + var url = '$effectiveHost:$selectedPort'; + if (effectiveHost == 'any') { url = 'localhost:$selectedPort'; } server._baseUri = Uri.http(url, server.basePath); - if (tlsCertPath != null && tlsCertKeyPath != null) { + if (effectiveCertPath != null && effectiveCertKeyPath != null) { server._baseUri = Uri.https(url, server.basePath); } if (testMode) { @@ -377,7 +378,7 @@ class WebAssetServer implements AssetReader { expressionCompiler: expressionCompiler, spawnDds: enableDds, ), - appMetadata: AppMetadata(hostname: hostname), + appMetadata: AppMetadata(hostname: effectiveHost), ), // Use DWDS WebSocket-based connection instead of Chrome-based connection for debugging useDwdsWebSocketConnection: useDwdsWebSocketConnection, @@ -386,6 +387,7 @@ class WebAssetServer implements AssetReader { if (shouldEnableMiddleware) { pipeline = pipeline.addMiddleware(middleware).addMiddleware(dwds.middleware); } + pipeline = pipeline.addMiddleware(proxyMiddleware(effectiveProxy)); final shelf.Handler dwdsHandler = pipeline.addHandler(server.handleRequest); final shelf.Cascade cascade = shelf.Cascade().add(dwds.handler).add(dwdsHandler); runZonedGuarded( diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 971affad9e63d..2da27e8b9b6a1 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -281,7 +281,6 @@ abstract class FlutterCommand extends Command { ); argParser.addOption( 'web-hostname', - defaultsTo: 'localhost', help: 'The hostname that the web server will use to resolve an IP to serve ' 'from. The unresolved hostname is used to launch Chrome when using ' diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 97b371ccfa691..99d98798e4df2 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -12,12 +12,13 @@ dependencies: # https://github.com/flutter/flutter/blob/main/docs/infra/Updating-dependencies-in-Flutter.md archive: 3.6.1 args: 2.7.0 + build_runner: ^2.5.4 dds: 5.0.4 dwds: 24.4.0 code_builder: 4.10.1 collection: 1.19.1 completion: 1.0.1 - coverage: 1.14.1 + coverage: 1.15.0 crypto: 3.0.6 ffi: 2.1.4 file: 7.0.1 @@ -45,6 +46,8 @@ dependencies: stream_channel: 2.1.4 shelf_web_socket: 2.0.1 shelf_static: 1.1.3 + shelf_router: ^1.1.4 + shelf_test_handler: ^2.0.2 pub_semver: 2.2.0 pool: 1.5.1 path: 1.9.1 @@ -113,6 +116,7 @@ dependencies: web: 1.1.1 web_socket: 1.0.1 yaml_edit: 2.2.2 + mockito: ^5.4.6 dev_dependencies: file_testing: 3.0.2 @@ -126,4 +130,5 @@ dev_dependencies: dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: kr614i + +# PUBSPEC CHECKSUM: p2iphr diff --git a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart index 48235e6d2255f..26da2cc6294c5 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart @@ -584,7 +584,7 @@ void main() { throwsToolExit(), ); - final DebuggingOptions options = await command.createDebuggingOptions(false); + final DebuggingOptions options = await command.createDebuggingOptions(); expect(options.startPaused, true); expect(options.disableServiceAuthCodes, true); @@ -634,7 +634,7 @@ void main() { throwsToolExit(), ); - final DebuggingOptions options = await command.createDebuggingOptions(false); + final DebuggingOptions options = await command.createDebuggingOptions(); expect(options.disablePortPublication, false); }, overrides: { @@ -670,7 +670,7 @@ void main() { ..connectionInterface = DeviceConnectionInterface.attached; fakeDeviceManager.attachedDevices = [usbDevice]; - final DebuggingOptions options = await command.createDebuggingOptions(false); + final DebuggingOptions options = await command.createDebuggingOptions(); expect(options.disablePortPublication, true); }, overrides: { @@ -706,7 +706,7 @@ void main() { throwsToolExit(), ); - final DebuggingOptions options = await command.createDebuggingOptions(false); + final DebuggingOptions options = await command.createDebuggingOptions(); expect(options.disablePortPublication, true); }, overrides: { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart index 62e47264ddd4c..bbd9a30162c28 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart @@ -15,18 +15,21 @@ import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/terminal.dart'; +import 'package:flutter_tools/src/base/time.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/daemon.dart'; import 'package:flutter_tools/src/commands/run.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:flutter_tools/src/web/compile.dart'; +import 'package:flutter_tools/src/web/web_runner.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart' as analytics; import 'package:vm_service/vm_service.dart'; @@ -396,7 +399,7 @@ void main() { isNull, ); - final DebuggingOptions options = await command.createDebuggingOptions(false); + final DebuggingOptions options = await command.createDebuggingOptions(); expect(options.uninstallFirst, isTrue); }, overrides: { @@ -931,7 +934,11 @@ void main() { }); group('--web-header', () { + late FakeWebRunnerFactory fakeWebRunnerFactory; + setUp(() { + fakeWebRunnerFactory = FakeWebRunnerFactory(); + fileSystem.file('lib/main.dart').createSync(recursive: true); fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('.dart_tool/package_config.json') @@ -942,7 +949,11 @@ void main() { "configVersion": 2 } '''); - final device = FakeDevice(isLocalEmulator: true, platformType: PlatformType.android); + final device = FakeDevice( + isLocalEmulator: true, + platformType: PlatformType.web, + targetPlatform: TargetPlatform.web_javascript, + ); testDeviceManager.devices = [device]; }); @@ -957,14 +968,19 @@ void main() { throwsToolExit(), ); - final DebuggingOptions options = await command.createDebuggingOptions(true); - expect(options.webHeaders, {'foo': 'bar'}); + expect(fakeWebRunnerFactory.lastOptions, isNotNull); + expect(fakeWebRunnerFactory.lastOptions!.devConfig, isNotNull); + expect(fakeWebRunnerFactory.lastOptions!.devConfig!.headers, { + 'foo': 'bar', + }); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), Logger: () => logger, DeviceManager: () => testDeviceManager, + FeatureFlags: () => FakeFeatureFlags(), + WebRunnerFactory: () => fakeWebRunnerFactory, }, ); @@ -975,17 +991,17 @@ void main() { await expectLater( () => createTestCommandRunner( command, - ).run(['run', '--no-pub', '--no-hot', '--web-header', 'foo']), + ).run(['run', '--no-pub', '--no-hot', '--no-resident', '--web-header', 'foo']), throwsToolExit(message: 'Invalid web headers: foo'), ); - - await expectLater(() => command.createDebuggingOptions(true), throwsToolExit()); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), Logger: () => logger, DeviceManager: () => testDeviceManager, + FeatureFlags: () => FakeFeatureFlags(), + WebRunnerFactory: () => fakeWebRunnerFactory, }, ); @@ -1002,14 +1018,10 @@ void main() { 'run', '--no-pub', '--no-hot', + '--no-resident', '--web-header', 'hurray/headers=flutter', ]), - throwsToolExit(), - ); - - await expectLater( - () => command.createDebuggingOptions(true), throwsToolExit(message: 'Invalid web headers: hurray/headers=flutter'), ); }, @@ -1018,15 +1030,20 @@ void main() { ProcessManager: () => FakeProcessManager.any(), Logger: () => logger, DeviceManager: () => testDeviceManager, + FeatureFlags: () => FakeFeatureFlags(), + WebRunnerFactory: () => fakeWebRunnerFactory, }, ); testUsingContext( 'throws a ToolExit when using --wasm on a non-web platform', () async { + testDeviceManager.devices = [FakeDevice(platformType: PlatformType.android)]; final command = RunCommand(); await expectLater( - () => createTestCommandRunner(command).run(['run', '--no-pub', '--wasm']), + () => createTestCommandRunner( + command, + ).run(['run', '--no-pub', '--no-resident', '--wasm']), throwsToolExit(message: '--wasm is only supported on the web platform'), ); }, @@ -1035,6 +1052,8 @@ void main() { ProcessManager: () => FakeProcessManager.any(), Logger: () => logger, DeviceManager: () => testDeviceManager, + FeatureFlags: () => FakeFeatureFlags(), + WebRunnerFactory: () => fakeWebRunnerFactory, }, ); @@ -1043,9 +1062,12 @@ void main() { () async { final command = RunCommand(); await expectLater( - () => createTestCommandRunner( - command, - ).run(['run', '--no-pub', ...WebRendererMode.skwasm.toCliDartDefines]), + () => createTestCommandRunner(command).run([ + 'run', + '--no-pub', + '--no-resident', + ...WebRendererMode.skwasm.toCliDartDefines, + ]), throwsToolExit(message: 'Skwasm renderer requires --wasm'), ); }, @@ -1054,6 +1076,8 @@ void main() { ProcessManager: () => FakeProcessManager.any(), Logger: () => logger, DeviceManager: () => testDeviceManager, + FeatureFlags: () => FakeFeatureFlags(), + WebRunnerFactory: () => fakeWebRunnerFactory, }, ); @@ -1072,17 +1096,24 @@ void main() { throwsToolExit(), ); - final DebuggingOptions options = await command.createDebuggingOptions(true); - expect(options.webHeaders, {'hurray': 'flutter,flutter=hurray'}); + expect(fakeWebRunnerFactory.lastOptions, isNotNull); + expect(fakeWebRunnerFactory.lastOptions!.devConfig, isNotNull); + expect(fakeWebRunnerFactory.lastOptions!.devConfig!.headers, { + 'hurray': 'flutter,flutter=hurray', + }); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), Logger: () => logger, DeviceManager: () => testDeviceManager, + FeatureFlags: () => FakeFeatureFlags(), + WebRunnerFactory: () => fakeWebRunnerFactory, }, ); }); + + // ... (rest of the file) }); group('terminal', () { @@ -1241,7 +1272,7 @@ void main() { throwsToolExit(), ); - final DebuggingOptions options = await command.createDebuggingOptions(true); + final DebuggingOptions options = await command.createDebuggingOptions(); expect(options.webUseSseForDebugBackend, false); expect(options.webUseSseForDebugProxy, false); @@ -1283,7 +1314,7 @@ void main() { throwsToolExit(), ); - final DebuggingOptions options = await command.createDebuggingOptions(false); + final DebuggingOptions options = await command.createDebuggingOptions(); expect(options.startPaused, true); expect(options.disableServiceAuthCodes, true); @@ -1319,7 +1350,7 @@ void main() { throwsToolExit(), ); - final DebuggingOptions options = await command.createDebuggingOptions(false); + final DebuggingOptions options = await command.createDebuggingOptions(); expect(options.usingCISystem, true); }, @@ -1340,7 +1371,7 @@ void main() { throwsToolExit(), ); - final DebuggingOptions options = await command.createDebuggingOptions(false); + final DebuggingOptions options = await command.createDebuggingOptions(); expect(options.webUseWasm, true); expect(options.webRenderer, WebRendererMode.skwasm); @@ -1369,7 +1400,7 @@ void main() { ), ); - final DebuggingOptions options = await command.createDebuggingOptions(true); + final DebuggingOptions options = await command.createDebuggingOptions(); expect(options.webLaunchUrl, 'http://flutter.dev'); final pattern = RegExp(r'^((http)?:\/\/)[^\s]+'); @@ -1482,6 +1513,7 @@ class FakeDevice extends Fake implements Device { @override String get displayName => name; + // THIS IS A KEY FIX @override Future get targetPlatform async => _targetPlatform; @@ -1693,3 +1725,38 @@ class FakeAnsiTerminal extends Fake implements AnsiTerminal { @override bool get singleCharMode => setSingleCharModeHistory.last; } + +/// A Fake that implements FeatureFlags and enables web. +class FakeFeatureFlags extends Fake implements FeatureFlags { + @override + bool get isWebEnabled => true; + + @override + bool isEnabled(Feature feature) => feature.master.enabledByDefault; +} + +/// A Fake WebRunnerFactory that CAPTURES the debugging options passed to it. +class FakeWebRunnerFactory extends Fake implements WebRunnerFactory { + DebuggingOptions? lastOptions; + + @override + ResidentRunner createWebRunner( + FlutterDevice device, { + String? target, + required bool stayResident, + required DebuggingOptions debuggingOptions, + required analytics.Analytics analytics, + required FileSystem fileSystem, + required FlutterProject flutterProject, + required Logger logger, + required OutputPreferences outputPreferences, + required Platform platform, + required SystemClock systemClock, + required Terminal terminal, + bool machine = false, + Future Function(String)? urlTunneller, + }) { + lastOptions = debuggingOptions; + return FakeResidentRunner(); + } +} diff --git a/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart b/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart index 9c5b83b8f3146..1f04254e40ce7 100644 --- a/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart +++ b/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart @@ -14,6 +14,7 @@ import 'package:flutter_tools/src/base/time.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/drive/web_driver_service.dart'; +import 'package:flutter_tools/src/isolated/devfs_config.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/resident_runner.dart'; @@ -277,13 +278,14 @@ void main() { final WebDriverService service = setUpDriverService(); final device = FakeDevice(); final webHeaders = {'test-header': 'test-value'}; + final devConfig = DevConfig(headers: webHeaders); await service.start( BuildInfo.profile, device, - DebuggingOptions.enabled(BuildInfo.profile, webHeaders: webHeaders, ipv6: true), + DebuggingOptions.enabled(BuildInfo.profile, devConfig: devConfig, ipv6: true), ); await service.stop(); - expect(FakeResidentRunner.instance.debuggingOptions.webHeaders, equals(webHeaders)); + expect(FakeResidentRunner.instance.debuggingOptions.devConfig?.headers, equals(webHeaders)); }, overrides: {WebRunnerFactory: () => FakeWebRunnerFactory()}, ); diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index 10771e2d9ffdc..730c0557ce4b2 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -24,6 +24,7 @@ import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:flutter_tools/src/isolated/devfs_config.dart'; import 'package:flutter_tools/src/isolated/devfs_web.dart'; import 'package:flutter_tools/src/isolated/resident_web_runner.dart'; import 'package:flutter_tools/src/project.dart'; @@ -1867,8 +1868,10 @@ flutter: 'throws when port is an integer outside the valid TCP range', () async { final logger = BufferLogger.test(); + const devConfig = DevConfig(port: 65536); + const devConfig2 = DevConfig(port: -1); - var debuggingOptions = DebuggingOptions.enabled(BuildInfo.debug, port: '65536'); + var debuggingOptions = DebuggingOptions.enabled(BuildInfo.debug, devConfig: devConfig); ResidentRunner residentWebRunner = setUpResidentRunner( flutterDevice, logger: logger, @@ -1876,7 +1879,7 @@ flutter: ); await expectToolExitLater(residentWebRunner.run(), matches('Invalid port: 65536.*')); - debuggingOptions = DebuggingOptions.enabled(BuildInfo.debug, port: '-1'); + debuggingOptions = DebuggingOptions.enabled(BuildInfo.debug, devConfig: devConfig2); residentWebRunner = setUpResidentRunner( flutterDevice, logger: logger, diff --git a/packages/flutter_tools/test/general.shard/web/devfs_web_ddc_modules_test.dart b/packages/flutter_tools/test/general.shard/web/devfs_web_ddc_modules_test.dart index f126800ab13cb..750e5d934d568 100644 --- a/packages/flutter_tools/test/general.shard/web/devfs_web_ddc_modules_test.dart +++ b/packages/flutter_tools/test/general.shard/web/devfs_web_ddc_modules_test.dart @@ -17,6 +17,7 @@ import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:flutter_tools/src/isolated/devfs_config.dart'; import 'package:flutter_tools/src/isolated/devfs_web.dart'; import 'package:flutter_tools/src/isolated/release_asset_server.dart'; import 'package:flutter_tools/src/isolated/web_asset_server.dart'; @@ -783,11 +784,8 @@ void main() { final ResidentCompiler residentCompiler = FakeResidentCompiler() ..output = const CompilerOutput('a', 0, []); + const devConfig = DevConfig(); final webDevFS = WebDevFS( - hostname: 'localhost', - port: 0, - tlsCertPath: null, - tlsCertKeyPath: null, packagesFilePath: '.dart_tool/package_config.json', urlTunneller: null, useSseForDebugProxy: true, @@ -805,7 +803,6 @@ void main() { entrypoint: Uri.base, testMode: true, expressionCompiler: null, - extraHeaders: const {}, chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, @@ -813,6 +810,7 @@ void main() { isWasm: false, useLocalCanvasKit: false, rootDirectory: globals.fs.currentDirectory, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, @@ -892,12 +890,8 @@ void main() { outputFile.parent.childFile('a.map').writeAsStringSync('{}'); outputFile.parent.childFile('a.metadata').writeAsStringSync('{}'); + const devConfig = DevConfig(); final webDevFS = WebDevFS( - // if this is any other value, we will do a real ip lookup - hostname: 'any', - port: 0, - tlsCertPath: null, - tlsCertKeyPath: null, packagesFilePath: '.dart_tool/package_config.json', urlTunneller: null, useSseForDebugProxy: true, @@ -915,7 +909,6 @@ void main() { entrypoint: Uri.base, testMode: true, expressionCompiler: null, - extraHeaders: const {}, chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, @@ -923,6 +916,7 @@ void main() { isWasm: false, useLocalCanvasKit: false, rootDirectory: globals.fs.currentDirectory, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, @@ -974,11 +968,8 @@ void main() { outputFile.parent.childFile('a.json').writeAsStringSync('{}'); outputFile.parent.childFile('a.map').writeAsStringSync('{}'); + const devConfig = DevConfig(); final webDevFS = WebDevFS( - hostname: 'any', - port: 0, - tlsCertPath: null, - tlsCertKeyPath: null, packagesFilePath: '.dart_tool/package_config.json', urlTunneller: null, useSseForDebugProxy: true, @@ -990,7 +981,6 @@ void main() { entrypoint: Uri.base, testMode: true, expressionCompiler: null, - extraHeaders: const {}, chromiumLauncher: null, nativeNullAssertions: true, ddcModuleSystem: usesDdcModuleSystem, @@ -999,6 +989,7 @@ void main() { isWasm: false, useLocalCanvasKit: false, rootDirectory: globals.fs.currentDirectory, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, @@ -1019,11 +1010,8 @@ void main() { outputFile.parent.childFile('a.json').writeAsStringSync('{}'); outputFile.parent.childFile('a.map').writeAsStringSync('{}'); + const devConfig = DevConfig(); final webDevFS = WebDevFS( - hostname: 'localhost', - port: 0, - tlsCertPath: null, - tlsCertKeyPath: null, packagesFilePath: '.dart_tool/package_config.json', urlTunneller: null, useSseForDebugProxy: true, @@ -1042,7 +1030,6 @@ void main() { entrypoint: Uri.base, testMode: true, expressionCompiler: null, - extraHeaders: const {}, chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, @@ -1050,6 +1037,7 @@ void main() { isWasm: false, useLocalCanvasKit: false, rootDirectory: globals.fs.currentDirectory, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, @@ -1077,11 +1065,8 @@ void main() { final String dummyCertPath = globals.fs.path.join(dataPath, 'tls_cert', 'dummy-cert.pem'); final String dummyCertKeyPath = globals.fs.path.join(dataPath, 'tls_cert', 'dummy-key.pem'); + final devConfig = DevConfig(https: HttpsConfig(certPath: dummyCertPath, certKeyPath: dummyCertKeyPath)); final webDevFS = WebDevFS( - hostname: 'localhost', - port: 0, - tlsCertPath: dummyCertPath, - tlsCertKeyPath: dummyCertKeyPath, packagesFilePath: '.dart_tool/package_config.json', urlTunneller: null, useSseForDebugProxy: true, @@ -1094,7 +1079,6 @@ void main() { entrypoint: Uri.base, testMode: true, expressionCompiler: null, - extraHeaders: const {}, chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, @@ -1102,6 +1086,7 @@ void main() { isWasm: false, useLocalCanvasKit: false, rootDirectory: globals.fs.currentDirectory, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, @@ -1120,11 +1105,8 @@ void main() { test( 'allows frame embedding', () => testbed.run(() async { + const devConfig = DevConfig(); final WebAssetServer webAssetServer = await WebAssetServer.start( - null, - 'localhost', - 0, - null, null, null, true, @@ -1140,11 +1122,11 @@ void main() { false, Uri.base, null, - const {}, webRenderer: WebRendererMode.canvaskit, isWasm: false, useLocalCanvasKit: false, testMode: true, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, @@ -1160,11 +1142,8 @@ void main() { () => testbed.run(() async { const extraHeaderKey = 'hurray'; const extraHeaderValue = 'flutter'; + const devConfig = DevConfig(headers: {extraHeaderKey: extraHeaderValue}); final WebAssetServer webAssetServer = await WebAssetServer.start( - null, - 'localhost', - 0, - null, null, null, true, @@ -1180,11 +1159,11 @@ void main() { false, Uri.base, null, - const {extraHeaderKey: extraHeaderValue}, webRenderer: WebRendererMode.canvaskit, isWasm: false, useLocalCanvasKit: false, testMode: true, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, @@ -1250,11 +1229,8 @@ void main() { outputFile.parent.childFile('a.map').writeAsStringSync('{}'); outputFile.parent.childFile('a.metadata').writeAsStringSync('{}'); + const devConfig = DevConfig(); final webDevFS = WebDevFS( - hostname: 'localhost', - port: 0, - tlsCertPath: null, - tlsCertKeyPath: null, packagesFilePath: '.dart_tool/package_config.json', urlTunneller: null, useSseForDebugProxy: true, @@ -1267,7 +1243,6 @@ void main() { entrypoint: Uri.base, testMode: true, expressionCompiler: null, - extraHeaders: const {}, chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, @@ -1275,6 +1250,7 @@ void main() { isWasm: false, useLocalCanvasKit: false, rootDirectory: globals.fs.currentDirectory, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, diff --git a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart index 4293af6213334..e0b9d9091eb4a 100644 --- a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart +++ b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart @@ -17,6 +17,7 @@ import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:flutter_tools/src/isolated/devfs_config.dart'; import 'package:flutter_tools/src/isolated/devfs_web.dart'; import 'package:flutter_tools/src/isolated/release_asset_server.dart'; import 'package:flutter_tools/src/isolated/web_asset_server.dart'; @@ -908,11 +909,8 @@ void main() { final ResidentCompiler residentCompiler = FakeResidentCompiler() ..output = const CompilerOutput('a', 0, []); + const devConfig = DevConfig(); final webDevFS = WebDevFS( - hostname: 'localhost', - port: 0, - tlsCertPath: null, - tlsCertKeyPath: null, packagesFilePath: '.dart_tool/package_config.json', urlTunneller: null, useSseForDebugProxy: true, @@ -930,7 +928,6 @@ void main() { entrypoint: Uri.base, testMode: true, expressionCompiler: null, - extraHeaders: const {}, chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, @@ -938,6 +935,7 @@ void main() { isWasm: false, useLocalCanvasKit: false, rootDirectory: globals.fs.currentDirectory, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, @@ -1018,11 +1016,8 @@ void main() { final ResidentCompiler residentCompiler = FakeResidentCompiler() ..output = const CompilerOutput('a', 0, []); + const devConfig = DevConfig(); final webDevFS = WebDevFS( - hostname: 'localhost', - port: 0, - tlsCertPath: null, - tlsCertKeyPath: null, packagesFilePath: '.dart_tool/package_config.json', urlTunneller: null, useSseForDebugProxy: true, @@ -1040,7 +1035,6 @@ void main() { entrypoint: Uri.base, testMode: true, expressionCompiler: null, - extraHeaders: const {}, chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, @@ -1048,6 +1042,7 @@ void main() { isWasm: false, useLocalCanvasKit: false, rootDirectory: globals.fs.currentDirectory, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, @@ -1132,13 +1127,9 @@ void main() { outputFile.parent.childFile('a.json').writeAsStringSync('{}'); outputFile.parent.childFile('a.map').writeAsStringSync('{}'); outputFile.parent.childFile('a.metadata').writeAsStringSync('{}'); - + const devConfig = DevConfig(); final webDevFS = WebDevFS( // if this is any other value, we will do a real ip lookup - hostname: 'any', - port: 0, - tlsCertPath: null, - tlsCertKeyPath: null, packagesFilePath: '.dart_tool/package_config.json', urlTunneller: null, useSseForDebugProxy: true, @@ -1156,7 +1147,6 @@ void main() { entrypoint: Uri.base, testMode: true, expressionCompiler: null, - extraHeaders: const {}, chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, @@ -1164,6 +1154,7 @@ void main() { isWasm: false, useLocalCanvasKit: false, rootDirectory: globals.fs.currentDirectory, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, @@ -1217,12 +1208,8 @@ void main() { outputFile.parent.childFile('a.sources').writeAsStringSync(''); outputFile.parent.childFile('a.json').writeAsStringSync('{}'); outputFile.parent.childFile('a.map').writeAsStringSync('{}'); - + const devConfig = DevConfig(); final webDevFS = WebDevFS( - hostname: 'any', - port: 0, - tlsCertPath: null, - tlsCertKeyPath: null, packagesFilePath: '.dart_tool/package_config.json', urlTunneller: null, useSseForDebugProxy: true, @@ -1234,7 +1221,6 @@ void main() { entrypoint: Uri.base, testMode: true, expressionCompiler: null, - extraHeaders: const {}, chromiumLauncher: null, nativeNullAssertions: true, ddcModuleSystem: usesDdcModuleSystem, @@ -1243,15 +1229,14 @@ void main() { isWasm: false, useLocalCanvasKit: false, rootDirectory: globals.fs.currentDirectory, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, ); webDevFS.requireJS.createSync(recursive: true); - webDevFS.stackTraceMapper.createSync(recursive: true); final Uri uri = await webDevFS.create(); - expect(uri.host, 'localhost'); await webDevFS.destroy(); }), @@ -1265,12 +1250,8 @@ void main() { outputFile.parent.childFile('a.sources').writeAsStringSync(''); outputFile.parent.childFile('a.json').writeAsStringSync('{}'); outputFile.parent.childFile('a.map').writeAsStringSync('{}'); - + const devConfig = DevConfig(); final webDevFS = WebDevFS( - hostname: 'localhost', - port: 0, - tlsCertPath: null, - tlsCertKeyPath: null, packagesFilePath: '.dart_tool/package_config.json', urlTunneller: null, useSseForDebugProxy: true, @@ -1289,7 +1270,6 @@ void main() { entrypoint: Uri.base, testMode: true, expressionCompiler: null, - extraHeaders: const {}, chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, @@ -1297,6 +1277,7 @@ void main() { isWasm: false, useLocalCanvasKit: false, rootDirectory: globals.fs.currentDirectory, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, @@ -1326,12 +1307,10 @@ void main() { final String dummyCertPath = globals.fs.path.join(dataPath, 'tls_cert', 'dummy-cert.pem'); final String dummyCertKeyPath = globals.fs.path.join(dataPath, 'tls_cert', 'dummy-key.pem'); - + final devConfig = DevConfig( + https: HttpsConfig(certPath: dummyCertPath, certKeyPath: dummyCertKeyPath), + ); final webDevFS = WebDevFS( - hostname: 'localhost', - port: 0, - tlsCertPath: dummyCertPath, - tlsCertKeyPath: dummyCertKeyPath, packagesFilePath: '.dart_tool/package_config.json', urlTunneller: null, useSseForDebugProxy: true, @@ -1344,7 +1323,6 @@ void main() { entrypoint: Uri.base, testMode: true, expressionCompiler: null, - extraHeaders: const {}, chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, @@ -1352,13 +1330,13 @@ void main() { isWasm: false, useLocalCanvasKit: false, rootDirectory: globals.fs.currentDirectory, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); - final Uri uri = await webDevFS.create(); // Ensure the connection established is secure @@ -1371,11 +1349,10 @@ void main() { test( 'allows frame embedding', () => testbed.run(() async { + // Wrap the original async block in testbed.run() + const devConfig = DevConfig(); + final WebAssetServer webAssetServer = await WebAssetServer.start( - null, - 'localhost', - 0, - null, null, null, true, @@ -1391,11 +1368,11 @@ void main() { false, Uri.base, null, - const {}, webRenderer: WebRendererMode.canvaskit, isWasm: false, useLocalCanvasKit: false, testMode: true, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, @@ -1405,17 +1382,14 @@ void main() { await webAssetServer.dispose(); }, overrides: {Artifacts: () => Artifacts.test()}), ); - test( 'passes on extra headers', () => testbed.run(() async { const extraHeaderKey = 'hurray'; const extraHeaderValue = 'flutter'; + const devConfig = DevConfig(headers: {extraHeaderKey: extraHeaderValue}); + final WebAssetServer webAssetServer = await WebAssetServer.start( - null, - 'localhost', - 0, - null, null, null, true, @@ -1431,11 +1405,11 @@ void main() { false, Uri.base, null, - const {extraHeaderKey: extraHeaderValue}, webRenderer: WebRendererMode.canvaskit, isWasm: false, useLocalCanvasKit: false, testMode: true, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, @@ -1446,7 +1420,6 @@ void main() { await webAssetServer.dispose(); }, overrides: {Artifacts: () => Artifacts.test()}), ); - test( 'WebAssetServer responds to POST requests with 404 not found', () => testbed.run(() async { @@ -1509,12 +1482,8 @@ void main() { outputFile.parent.childFile('a.json').writeAsStringSync('{}'); outputFile.parent.childFile('a.map').writeAsStringSync('{}'); outputFile.parent.childFile('a.metadata').writeAsStringSync('{}'); - + const devConfig = DevConfig(); final webDevFS = WebDevFS( - hostname: 'localhost', - port: 0, - tlsCertPath: null, - tlsCertKeyPath: null, packagesFilePath: '.dart_tool/package_config.json', urlTunneller: null, useSseForDebugProxy: true, @@ -1527,7 +1496,6 @@ void main() { entrypoint: Uri.base, testMode: true, expressionCompiler: null, - extraHeaders: const {}, chromiumLauncher: null, ddcModuleSystem: usesDdcModuleSystem, canaryFeatures: canaryFeatures, @@ -1535,6 +1503,7 @@ void main() { isWasm: false, useLocalCanvasKit: false, rootDirectory: globals.fs.currentDirectory, + devConfig: devConfig, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, diff --git a/packages/flutter_tools/test/general.shard/web/proxy_test.dart b/packages/flutter_tools/test/general.shard/web/proxy_test.dart new file mode 100644 index 0000000000000..38fe552332fb7 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/web/proxy_test.dart @@ -0,0 +1,448 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:flutter_tools/src/isolated/devfs_proxy.dart'; +import 'package:shelf/shelf.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +import '../../src/testbed.dart'; + +void main() { + late TestBed testbed; + setUp(() { + testbed = TestBed(); + }); + + group('ProxyRule.fromYaml', () { + test( + 'should create SourceProxyRule with source and no replacement', + () => testbed.run(() { + final yaml = + loadYaml(''' + target: http://localhost:8080 + source: /api + ''') + as YamlMap; + final ProxyRule? rule = ProxyRule.fromYaml(yaml); + + expect(rule, isA()); + expect((rule! as SourceProxyRule).source, '/api'); + expect(rule.target, 'http://localhost:8080'); + }), + ); + + test( + 'should create SourceProxyRule with source and replacement', + () => testbed.run(() { + final yaml = + loadYaml(''' + target: http://localhost:8080 + source: /api + replace: /new_api + ''') + as YamlMap; + final ProxyRule? rule = ProxyRule.fromYaml(yaml); + + expect(rule, isA()); + expect((rule! as SourceProxyRule).source, '/api'); + expect(rule.target, 'http://localhost:8080'); + expect(rule.replace('/api/users'), '/new_api/users'); + expect(rule.replace('/api/'), '/new_api/'); + expect(rule.replace('/other'), '/other'); + }), + ); + + test( + 'should create RegexProxyRule with regex and no replacement', + () => testbed.run(() { + final yaml = + loadYaml(r''' + target: http://localhost:8081 + regex: ^/users/(\d+) + ''') + as YamlMap; + final ProxyRule? rule = ProxyRule.fromYaml(yaml); + + expect(rule, isA()); + expect((rule! as RegexProxyRule).pattern.pattern, r'^/users/(\d+)'); + expect(rule.target, 'http://localhost:8081'); + }), + ); + + test( + 'should create RegexProxyRule with regex and replacement using capturing groups', + () => testbed.run(() { + final yaml = + loadYaml(r''' + target: http://localhost:8081/user-service + regex: ^/users/(\d+)/profile(.*) + replace: /user-info/$1/details$2 + ''') + as YamlMap; + final ProxyRule? rule = ProxyRule.fromYaml(yaml); + expect(rule, isA()); + expect(rule!.replace('/users/456/profile/summary'), '/user-info/456/details/summary'); + expect(rule.replace('/users/789/profile'), '/user-info/789/details'); + expect(rule.replace('/other/path'), '/other/path'); + }), + ); + + test( + 'should create RegexProxyRule with regex and empty replacement', + () => testbed.run(() { + final yaml = + loadYaml(r''' + target: http://localhost:8081/user-service + regex: ^/users/\d+/profile + replace: '' + ''') + as YamlMap; + final ProxyRule? rule = ProxyRule.fromYaml(yaml); + expect(rule, isA()); + expect(rule!.replace('/users/456/profile'), ''); + expect(rule.replace('/users/789/profile/summary'), '/summary'); + }), + ); + + test( + 'should handle invalid regex key gracefully and fall back to RegexProxyRule using escaped string', + () => testbed.run(() { + final yaml = + loadYaml(''' + target: http://localhost:8082 + regex: ^/invalid( + ''') + as YamlMap; + final ProxyRule? rule = ProxyRule.fromYaml(yaml, logger: globals.logger); + + expect(rule, isA()); + expect((rule! as RegexProxyRule).pattern.pattern, r'\^/invalid\('); + expect(rule.target, 'http://localhost:8082'); + }), + ); + + test( + 'should return null if target is missing', + () => testbed.run(() { + final yaml = + loadYaml(''' + source: /api + ''') + as YamlMap; + final ProxyRule? rule = ProxyRule.fromYaml(yaml, logger: globals.logger); + + expect(rule, isNull); + }), + ); + + test( + 'should return null if neither source nor regex is provided', + () => testbed.run(() { + final yaml = + loadYaml(''' + target: http://localhost:8080 + ''') + as YamlMap; + final ProxyRule? rule = ProxyRule.fromYaml(yaml, logger: globals.logger); + + expect(rule, isNull); + }), + ); + }); + + group('RegexProxyRule', () { + final ruleNoReplacement = RegexProxyRule( + pattern: RegExp(r'^/users/(\d+)'), + target: 'http://example.com', + ); + + final ruleWithCapturingGroupReplacement = RegexProxyRule( + pattern: RegExp(r'^/api/v1/users/(\d+)(.*)'), + target: 'http://backend.com', + replacement: r'/$1/profile$2', + ); + + final rulePrefixRemovalReplacement = RegexProxyRule( + pattern: RegExp(r'^/old_path'), + target: 'http://legacy.com', + replacement: '/new_path', + ); + final ruleMiddlePattern = RegexProxyRule( + pattern: RegExp(r'/test_static'), + target: 'http://static.com', + replacement: '/assets', + ); + + final ruleExactMatch = RegexProxyRule( + pattern: RegExp(r'^/exact_match_only$'), + target: 'http://exact.com', + replacement: '/found', + ); + final ruleZeroGroup = RegexProxyRule( + pattern: RegExp(r'^/prefix/(.*)'), + target: 'http://test.com', + replacement: r'/all$0', + ); + + test('matches should return true for matching regex', () { + expect(ruleNoReplacement.matches('/users/123'), isTrue); + expect( + ruleWithCapturingGroupReplacement.matches('/api/v1/users/456/profile/details'), + isTrue, + ); + expect(rulePrefixRemovalReplacement.matches('/old_path/resource'), isTrue); + expect(ruleMiddlePattern.matches('hello/test_static/image.png'), isTrue); + expect(ruleExactMatch.matches('/exact_match_only'), isTrue); + expect(ruleZeroGroup.matches('/prefix/prefix'), isTrue); + }); + + test('matches should return false for non-matching regex', () { + expect(ruleWithCapturingGroupReplacement.matches('/api/v2/users/123'), isFalse); + expect(rulePrefixRemovalReplacement.matches('/hello/old_path/resource'), isFalse); + expect(ruleExactMatch.matches('/exact_match_only/suffix'), isFalse); + }); + + test('replace should return original path if no replacement string', () { + expect(ruleNoReplacement.replace('/users/123/data'), '/users/123/data'); + }); + + test('replace should apply replacement with capturing groups correctly', () { + expect( + ruleWithCapturingGroupReplacement.replace('/api/v1/users/789/profile/summary'), + '/789/profile/profile/summary', + ); + expect(ruleWithCapturingGroupReplacement.replace('/api/v1/users/100'), '/100/profile'); + }); + + test('replace should apply prefix removal replacement', () { + expect( + rulePrefixRemovalReplacement.replace('/old_path/resource/data'), + '/new_path/resource/data', + ); + expect(rulePrefixRemovalReplacement.replace('/old_path'), '/new_path'); + }); + + test('replace should match exactly', () { + final rule = RegexProxyRule( + pattern: RegExp(r'/temp1'), + target: 'http://legacy.com', + replacement: '/temp2/', + ); + expect(rule.replace('/temp1/careful/double/slashes'), '/temp2//careful/double/slashes'); + expect( + rulePrefixRemovalReplacement.replace('/old_pathname/resource/data'), + '/new_pathname/resource/data', + ); + }); + + test('replace should replace all occurences', () { + expect(ruleMiddlePattern.replace('/test_static/test_static/data'), '/assets/assets/data'); + }); + + test('replace should handle regex with no capturing groups in pattern', () { + expect( + ruleMiddlePattern.replace('hello/test_static/document.pdf'), + 'hello/assets/document.pdf', + ); + }); + + test(r'replace should handle $0 (entire match)', () { + expect(ruleZeroGroup.replace('/prefix/something/else'), '/all/prefix/something/else'); + }); + + test('replace should handle non-matching path gracefully', () { + expect(ruleWithCapturingGroupReplacement.replace('/non/matching/path'), '/non/matching/path'); + }); + + test('toString provides useful debug information', () { + expect( + ruleNoReplacement.toString(), + r'{pattern: ^/users/(\d+), target: http://example.com, replacement: null}', + ); + expect( + rulePrefixRemovalReplacement.toString(), + '{pattern: ^/old_path, target: http://legacy.com, replacement: /new_path}', + ); + }); + }); + + group('SourceProxyRule', () { + final ruleNoReplacement = SourceProxyRule(source: '/assets/', target: 'http://cdn.example.com'); + + final ruleWithReplacement = SourceProxyRule( + source: '/old-assets/', + target: 'http://cdn.example.com', + replacement: '/new-assets/', + ); + + final ruleEmptyReplacement = SourceProxyRule( + source: '/remove-me/', + target: 'http://cdn.example.com', + replacement: '', + ); + final ruleSlashReplacement = SourceProxyRule( + source: '/remove-me-too', + target: 'http://cdn.example.com', + replacement: '/', + ); + + test('matches should return true for matching source', () { + expect(ruleNoReplacement.matches('/assets/image.png'), isTrue); + expect(ruleWithReplacement.matches('/old-assets/script.js'), isTrue); + expect(ruleEmptyReplacement.matches('/remove-me/now'), isTrue); + expect(ruleSlashReplacement.matches('/remove-me-too-please'), isTrue); + expect(ruleSlashReplacement.matches('/remove-me-too/please'), isTrue); + }); + + test('matches should return false for non-matching source', () { + expect(ruleNoReplacement.matches('/data/assets/image.png'), isFalse); + expect(ruleWithReplacement.matches('/old/assets/script.js'), isFalse); + expect(ruleWithReplacement.matches('/old-assets-prefix/script.js'), isFalse); + expect(ruleSlashReplacement.matches('remove-me-too/please'), isFalse); + }); + + test('replace should return original path if no replacement string', () { + expect(ruleNoReplacement.replace('/assets/document.pdf'), '/assets/document.pdf'); + }); + + test('replace should apply replacement for matching source', () { + expect(ruleWithReplacement.replace('/old-assets/style.css'), '/new-assets/style.css'); + expect(ruleWithReplacement.replace('/old-assets/'), '/new-assets/'); + }); + + test('replace should handle empty replacement string', () { + expect(ruleEmptyReplacement.replace('/remove-me/file.txt'), 'file.txt'); + expect(ruleEmptyReplacement.replace('/remove-me/'), ''); + }); + + test('replace should handle slash replacement string', () { + expect(ruleSlashReplacement.replace('/remove-me-too'), '/'); + }); + + test('replace should only replace first occurence', () { + expect( + ruleWithReplacement.replace('/old-assets/old-assets/style.css'), + '/new-assets/old-assets/style.css', + ); + expect(ruleSlashReplacement.replace('/remove-me-too/remove-me-too/'), '//remove-me-too/'); + }); + + test('replace should return original path for non-matching source', () { + expect(ruleWithReplacement.replace('/other-path/file.txt'), '/other-path/file.txt'); + }); + test('toString provides useful debug information', () { + expect( + ruleNoReplacement.toString(), + '{source: /assets/, target: http://cdn.example.com, replacement: null}', + ); + expect( + ruleWithReplacement.toString(), + '{source: /old-assets/, target: http://cdn.example.com, replacement: /new-assets/}', + ); + }); + }); + + group('proxyRequest', () { + test('should correctly proxy all request elements', () async { + final Uri originalUrl = Uri.parse('http://original.example.com/path'); + final Uri finalTargetUrl = Uri.parse('http://target.example.com/newpath'); + const originalBody = 'Hello, Shelf Proxy!'; + final originalHeaders = { + 'Content-Type': 'text/plain', + 'X-Custom-Header': 'value', + 'content-length': 'ignored', + }; + final originalContext = {'user': 'testuser', 'auth': true}; + + final originalRequest = Request( + 'POST', + originalUrl, + headers: originalHeaders, + body: originalBody, + context: originalContext, + ); + final Request proxiedRequest = proxyRequest(originalRequest, finalTargetUrl); + + final expectedHeadersFiltered = Map.fromEntries( + originalHeaders.entries.where( + (MapEntry entry) => entry.key.toLowerCase() != 'content-length', + ), + ); + + for (final MapEntry entry in expectedHeadersFiltered.entries) { + expect(proxiedRequest.headers, containsPair(entry.key, entry.value)); + } + + expect(proxiedRequest.method, 'POST'); + expect(proxiedRequest.url.toString(), 'newpath'); + expect(proxiedRequest.context, originalContext); + + final String proxiedBody = await proxiedRequest.readAsString(); + expect(proxiedBody, originalBody); + }); + + test('should handle an empty request body', () async { + final Uri originalUrl = Uri.parse('http://original.example.com/empty'); + final Uri finalTargetUrl = Uri.parse('http://target.example.com/empty-new'); + + final originalRequest = Request('GET', originalUrl); + + final Request proxiedRequest = proxyRequest(originalRequest, finalTargetUrl); + + expect(proxiedRequest.method, 'GET'); + expect(proxiedRequest.url.toString(), 'empty-new'); + expect(await proxiedRequest.readAsString(), ''); + }); + + test('should handle different HTTP methods', () async { + final Uri originalUrl = Uri.parse('http://original.example.com/data'); + final Uri finalTargetUrl = Uri.parse('http://target.example.com/api/data'); + final methods = ['PUT', 'DELETE', 'PATCH', 'GET']; + + for (final method in methods) { + final originalRequest = Request( + method, + originalUrl, + body: method == 'PUT' || method == 'PATCH' ? '{"key": "value"}' : null, + ); + + final Request proxiedRequest = proxyRequest(originalRequest, finalTargetUrl); + expect(proxiedRequest.method, method, reason: 'Method "$method" should be preserved'); + + if (method == 'PUT' || method == 'PATCH') { + expect(await proxiedRequest.readAsString(), '{"key": "value"}'); + } else { + expect(await proxiedRequest.readAsString(), ''); + } + } + }); + }); + + group('proxyMiddleware', () { + test('should call inner handler if no rule matches', () async { + final rules = [ + RegexProxyRule(pattern: RegExp(r'^/other_api'), target: 'http://mock-backend.com'), + ]; + + final Middleware middleware = proxyMiddleware(rules); + + var innerHandlerCalled = false; + FutureOr innerHandler(Request request) { + innerHandlerCalled = true; + return Response.ok('Inner Handler Response'); + } + + final request = Request('GET', Uri.parse('http://localhost:8080/non_matching_path')); + final Response response = await middleware(innerHandler)(request); + + expect(innerHandlerCalled, isTrue); + expect(response.statusCode, 200); + expect(await response.readAsString(), 'Inner Handler Response'); + }); + }); +} diff --git a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/pubspec.yaml b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/pubspec.yaml index af24074046f52..c145867524421 100644 --- a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/pubspec.yaml +++ b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: flutter_lints: 6.0.0 google_fonts: 6.2.1 stack_trace: 1.12.1 - url_launcher: 6.3.1 + url_launcher: 6.3.2 async: 2.13.0 boolean_selector: 2.1.2 @@ -62,4 +62,4 @@ dependencies: flutter: uses-material-design: true -# PUBSPEC CHECKSUM: mlb1k3 +# PUBSPEC CHECKSUM: jug770 diff --git a/pubspec.lock b/pubspec.lock index 98041bf013cb1..bf3ed0f3bbcc2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -150,10 +150,10 @@ packages: dependency: "direct main" description: name: coverage - sha256: aa07dbe5f2294c827b7edb9a87bba44a9c15a3cc81bc8da2ca19b37322d30080 + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" url: "https://pub.dev" source: hosted - version: "1.14.1" + version: "1.15.0" crypto: dependency: "direct main" description: @@ -510,10 +510,10 @@ packages: dependency: "direct main" description: name: native_toolchain_c - sha256: "99a7feeac19069bbaf3cf9a4d54f47fa54f3ea6da681f669a8761c80540a0838" + sha256: "74a0c80d877c519bc6bde2c4e27b6b01c1f93c9b480f65ceae8bedd3aba3c086" url: "https://pub.dev" source: hosted - version: "0.16.6" + version: "0.16.8" nested: dependency: "direct main" description: @@ -859,10 +859,10 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_android: dependency: "direct main" description: @@ -947,10 +947,10 @@ packages: dependency: "direct main" description: name: video_player_avfoundation - sha256: "0d47db6cbf72db61d86369219efd35c7f9d93515e1319da941ece81b1f21c49c" + sha256: "9fedd55023249f3a02738c195c906b4e530956191febf0838e37d0dac912f953" url: "https://pub.dev" source: hosted - version: "2.7.2" + version: "2.8.0" video_player_platform_interface: dependency: "direct main" description: @@ -963,10 +963,10 @@ packages: dependency: "direct main" description: name: video_player_web - sha256: e8bba2e5d1e159d5048c9a491bb2a7b29c535c612bb7d10c1e21107f5bd365ba + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.4.0" vm_service: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 55ef516d52b16..4ed6b531b59e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,7 +94,7 @@ dependencies: code_assets: 0.19.4 collection: 1.19.1 convert: 3.1.2 - coverage: 1.14.1 + coverage: 1.15.0 crypto: 3.0.6 csslib: 1.0.2 cupertino_icons: 1.0.8 @@ -134,7 +134,7 @@ dependencies: meta: 1.16.0 metrics_center: 1.0.13 mime: 2.0.0 - native_toolchain_c: 0.16.6 + native_toolchain_c: 0.16.8 nested: 1.0.0 node_preamble: 2.0.2 package_config: 2.2.0 @@ -175,7 +175,7 @@ dependencies: test_api: 0.7.6 test_core: 0.6.11 typed_data: 1.4.0 - url_launcher: 6.3.1 + url_launcher: 6.3.2 url_launcher_android: 6.3.16 url_launcher_ios: 6.3.3 url_launcher_linux: 3.2.1 @@ -186,9 +186,9 @@ dependencies: vector_math: 2.2.0 video_player: 2.10.0 video_player_android: 2.8.7 - video_player_avfoundation: 2.7.2 + video_player_avfoundation: 2.8.0 video_player_platform_interface: 6.4.0 - video_player_web: 2.3.5 + video_player_web: 2.4.0 vm_service: 15.0.2 vm_snapshot_analysis: 0.7.6 watcher: 1.1.2 @@ -213,4 +213,4 @@ dependencies: pedantic: 1.11.1 quiver: 3.2.2 yaml_edit: 2.2.2 -# PUBSPEC CHECKSUM: j61vfe +# PUBSPEC CHECKSUM: leubun pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy