diff --git a/dev/bots/suite_runners/run_web_long_running_tests.dart b/dev/bots/suite_runners/run_web_long_running_tests.dart index 4ce1f02b38..5ae5d0c275 100644 --- a/dev/bots/suite_runners/run_web_long_running_tests.dart +++ b/dev/bots/suite_runners/run_web_long_running_tests.dart @@ -32,6 +32,7 @@ Future webLongRunningTestsRunner(String flutterRoot) async { target: path.join('test_driver', 'failure.dart'), buildMode: buildMode, renderer: 'canvaskit', + wasm: false, // This test intentionally fails and prints stack traces in the browser // logs. To avoid confusion, silence browser output. silenceBrowserOutput: true, @@ -42,6 +43,17 @@ Future webLongRunningTestsRunner(String flutterRoot) async { driver: path.join('test_driver', 'integration_test.dart'), buildMode: buildMode, renderer: 'canvaskit', + wasm: false, + expectWriteResponseFile: true, + expectResponseFileContent: 'null', + ), + () => _runFlutterDriverWebTest( + testAppDirectory: path.join('packages', 'integration_test', 'example'), + target: path.join('integration_test', 'example_test.dart'), + driver: path.join('test_driver', 'integration_test.dart'), + buildMode: buildMode, + renderer: 'skwasm', + wasm: true, expectWriteResponseFile: true, expectResponseFileContent: 'null', ), @@ -51,6 +63,7 @@ Future webLongRunningTestsRunner(String flutterRoot) async { driver: path.join('test_driver', 'extended_integration_test.dart'), buildMode: buildMode, renderer: 'canvaskit', + wasm: false, expectWriteResponseFile: true, expectResponseFileContent: ''' { @@ -97,6 +110,7 @@ Future webLongRunningTestsRunner(String flutterRoot) async { () => _runWebE2eTest('capabilities_integration_canvaskit', buildMode: 'debug', renderer: 'auto'), () => _runWebE2eTest('capabilities_integration_canvaskit', buildMode: 'profile', renderer: 'canvaskit'), () => _runWebE2eTest('capabilities_integration_html', buildMode: 'release', renderer: 'html'), + () => _runWebE2eTest('capabilities_integration_skwasm', buildMode: 'release', renderer: 'skwasm', wasm: true), // This test doesn't do anything interesting w.r.t. rendering, so we don't run the full build mode x renderer matrix. // CacheWidth and CacheHeight are only currently supported in CanvasKit mode, so we don't run the test in HTML mode. @@ -110,6 +124,7 @@ Future webLongRunningTestsRunner(String flutterRoot) async { target: 'test_driver/smoke_web_engine.dart', buildMode: 'profile', renderer: 'auto', + wasm: false, ), () => _runGalleryE2eWebTest('debug'), () => _runGalleryE2eWebTest('debug', canvasKit: true), @@ -192,12 +207,14 @@ Future _runWebE2eTest( String name, { required String buildMode, required String renderer, + bool wasm = false, }) async { await _runFlutterDriverWebTest( target: path.join('test_driver', '$name.dart'), buildMode: buildMode, renderer: renderer, testAppDirectory: path.join(flutterRoot, 'dev', 'integration_tests', 'web_e2e_tests'), + wasm: wasm, ); } @@ -206,6 +223,7 @@ Future _runFlutterDriverWebTest({ required String buildMode, required String renderer, required String testAppDirectory, + required bool wasm, String? driver, bool expectFailure = false, bool silenceBrowserOutput = false, @@ -235,6 +253,7 @@ Future _runFlutterDriverWebTest({ 'web-server', '--$buildMode', '--web-renderer=$renderer', + if (wasm) '--wasm', ], expectNonZeroExit: expectFailure, workingDirectory: testAppDirectory, diff --git a/dev/integration_tests/non_nullable/web/flutter_bootstrap.js b/dev/integration_tests/non_nullable/web/flutter_bootstrap.js new file mode 100644 index 0000000000..be78d93ce8 --- /dev/null +++ b/dev/integration_tests/non_nullable/web/flutter_bootstrap.js @@ -0,0 +1,12 @@ +// 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. + +{{flutter_js}} +{{flutter_build_config}} +_flutter.loader.load({ + config: { + // Use the local CanvasKit bundle instead of the CDN to reduce test flakiness. + canvasKitBaseUrl: "/canvaskit/", + }, +}); diff --git a/dev/integration_tests/non_nullable/web/index.html b/dev/integration_tests/non_nullable/web/index.html index aae9e6efad..dd2f083573 100644 --- a/dev/integration_tests/non_nullable/web/index.html +++ b/dev/integration_tests/non_nullable/web/index.html @@ -22,16 +22,6 @@ found in the LICENSE file. --> - - - + diff --git a/dev/integration_tests/web_compile_tests/web/flutter_bootstrap.js b/dev/integration_tests/web_compile_tests/web/flutter_bootstrap.js new file mode 100644 index 0000000000..be78d93ce8 --- /dev/null +++ b/dev/integration_tests/web_compile_tests/web/flutter_bootstrap.js @@ -0,0 +1,12 @@ +// 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. + +{{flutter_js}} +{{flutter_build_config}} +_flutter.loader.load({ + config: { + // Use the local CanvasKit bundle instead of the CDN to reduce test flakiness. + canvasKitBaseUrl: "/canvaskit/", + }, +}); diff --git a/dev/integration_tests/web_compile_tests/web/index.html b/dev/integration_tests/web_compile_tests/web/index.html index a856086c8e..313adac93f 100644 --- a/dev/integration_tests/web_compile_tests/web/index.html +++ b/dev/integration_tests/web_compile_tests/web/index.html @@ -7,6 +7,6 @@ found in the LICENSE file. --> Hello, World - + diff --git a/dev/integration_tests/web_e2e_tests/test_driver/capabilities_integration_canvaskit.dart b/dev/integration_tests/web_e2e_tests/test_driver/capabilities_integration_canvaskit.dart index 2dbbfb58e0..46d8a8ae3a 100644 --- a/dev/integration_tests/web_e2e_tests/test_driver/capabilities_integration_canvaskit.dart +++ b/dev/integration_tests/web_e2e_tests/test_driver/capabilities_integration_canvaskit.dart @@ -12,5 +12,7 @@ void main() { testWidgets('isCanvasKit returns true in CanvasKit mode', (WidgetTester tester) async { await tester.pumpAndSettle(); expect(isCanvasKit, true); + expect(isSkwasm, false); + expect(isSkiaWeb, true); }); } diff --git a/dev/integration_tests/web_e2e_tests/test_driver/capabilities_integration_html.dart b/dev/integration_tests/web_e2e_tests/test_driver/capabilities_integration_html.dart index 1f0a11e339..d0830e1572 100644 --- a/dev/integration_tests/web_e2e_tests/test_driver/capabilities_integration_html.dart +++ b/dev/integration_tests/web_e2e_tests/test_driver/capabilities_integration_html.dart @@ -8,8 +8,10 @@ import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('isCanvasKit returns false in HTML mode', (WidgetTester tester) async { + testWidgets('capabilities are set properly in HTML mode', (WidgetTester tester) async { await tester.pumpAndSettle(); expect(isCanvasKit, false); + expect(isSkwasm, false); + expect(isSkiaWeb, false); }); } diff --git a/dev/integration_tests/web_e2e_tests/test_driver/capabilities_integration_skwasm.dart b/dev/integration_tests/web_e2e_tests/test_driver/capabilities_integration_skwasm.dart new file mode 100644 index 0000000000..c16afb2ce9 --- /dev/null +++ b/dev/integration_tests/web_e2e_tests/test_driver/capabilities_integration_skwasm.dart @@ -0,0 +1,18 @@ +// 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:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('capabilities are set properly in Skwasm mode', (WidgetTester tester) async { + await tester.pumpAndSettle(); + expect(isCanvasKit, false); + expect(isSkwasm, true); + expect(isSkiaWeb, true); + }); +} diff --git a/dev/integration_tests/web_e2e_tests/test_driver/capabilities_integration_skwasm_test.dart b/dev/integration_tests/web_e2e_tests/test_driver/capabilities_integration_skwasm_test.dart new file mode 100644 index 0000000000..b2d2a1770b --- /dev/null +++ b/dev/integration_tests/web_e2e_tests/test_driver/capabilities_integration_skwasm_test.dart @@ -0,0 +1,7 @@ +// 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:integration_test/integration_test_driver.dart' as test; + +Future main() async => test.integrationDriver(); diff --git a/examples/hello_world/web/flutter_bootstrap.js b/examples/hello_world/web/flutter_bootstrap.js new file mode 100644 index 0000000000..be78d93ce8 --- /dev/null +++ b/examples/hello_world/web/flutter_bootstrap.js @@ -0,0 +1,12 @@ +// 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. + +{{flutter_js}} +{{flutter_build_config}} +_flutter.loader.load({ + config: { + // Use the local CanvasKit bundle instead of the CDN to reduce test flakiness. + canvasKitBaseUrl: "/canvaskit/", + }, +}); diff --git a/examples/hello_world/web/index.html b/examples/hello_world/web/index.html index a856086c8e..313adac93f 100644 --- a/examples/hello_world/web/index.html +++ b/examples/hello_world/web/index.html @@ -7,6 +7,6 @@ found in the LICENSE file. --> Hello, World - + diff --git a/packages/flutter_tools/lib/src/commands/build_web.dart b/packages/flutter_tools/lib/src/commands/build_web.dart index db67a41531..7244c45204 100644 --- a/packages/flutter_tools/lib/src/commands/build_web.dart +++ b/packages/flutter_tools/lib/src/commands/build_web.dart @@ -64,7 +64,7 @@ class BuildWebCommand extends BuildSubCommand { help: 'Sets the optimization level used for Dart compilation to JavaScript/Wasm.', defaultsTo: '${WebCompilerConfig.kDefaultOptimizationLevel}', - allowed: const ['1', '2', '3', '4'], + allowed: const ['0', '1', '2', '3', '4'], ); // diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 88909281f3..548b9fc042 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -30,6 +30,7 @@ import '../runner/flutter_command_runner.dart'; import '../tracing.dart'; import '../vmservice.dart'; import '../web/compile.dart'; +import '../web/web_constants.dart'; import '../web/web_runner.dart'; import 'daemon.dart'; @@ -179,7 +180,12 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment hide: !verboseHelp, help: 'Uninstall previous versions of the app on the device ' 'before reinstalling. Currently only supported on iOS.', - ); + ) + ..addFlag( + FlutterOptions.kWebWasmFlag, + help: 'Compile to WebAssembly rather than JavaScript.\n$kWasmMoreInfo', + negatable: false, + ); usesWebOptions(verboseHelp: verboseHelp); usesTargetOption(); usesPortOptions(verboseHelp: verboseHelp); @@ -227,6 +233,13 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment String? get traceAllowlist => stringArg('trace-allowlist'); + bool get useWasm => boolArg(FlutterOptions.kWebWasmFlag); + + WebRendererMode get webRenderer => WebRendererMode.fromCliOption( + stringArg(FlutterOptions.kWebRendererFlag), + useWasm: useWasm + ); + /// Create a debugging options instance for the current `run` or `drive` invocation. @visibleForTesting @protected @@ -242,10 +255,6 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment final Map webHeaders = featureFlags.isWebEnabled ? extractWebHeaders() : const {}; - final String? webRendererString = stringArg('web-renderer'); - final WebRendererMode webRenderer = (webRendererString != null) - ? WebRendererMode.values.byName(webRendererString) - : WebRendererMode.auto; if (buildInfo.mode.isRelease) { return DebuggingOptions.disabled( @@ -264,6 +273,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment webBrowserFlags: webBrowserFlags, webHeaders: webHeaders, webRenderer: webRenderer, + webUseWasm: useWasm, enableImpeller: enableImpeller, enableVulkanValidation: enableVulkanValidation, uninstallFirst: uninstallFirst, @@ -314,6 +324,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment webLaunchUrl: featureFlags.isWebEnabled ? stringArg('web-launch-url') : null, webHeaders: webHeaders, webRenderer: webRenderer, + webUseWasm: useWasm, vmserviceOutFile: stringArg('vmservice-out-file'), fastStart: argParser.options.containsKey('fast-start') && boolArg('fast-start') @@ -630,12 +641,21 @@ class RunCommand extends RunCommandBase { if (devices!.any((Device device) => device is AndroidDevice)) { _deviceDeprecationBehavior = DeprecationBehavior.exit; } + // Only support "web mode" with a single web device due to resident runner // refactoring required otherwise. webMode = featureFlags.isWebEnabled && devices!.length == 1 && await devices!.single.targetPlatform == TargetPlatform.web_javascript; + if (useWasm && !webMode) { + throwToolExit('--wasm is only supported on the web platform'); + } + + if (webRenderer == WebRendererMode.skwasm && !useWasm) { + throwToolExit('Skwasm renderer requires --wasm'); + } + final String? flavor = stringArg('flavor'); final bool flavorsSupportedOnEveryDevice = devices! .every((Device device) => device.supportsFlavors); diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index abaa42f11f..b3cf671532 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -330,6 +330,11 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { return super.verifyThenRunCommand(commandPath); } + WebRendererMode get webRenderer => WebRendererMode.fromCliOption( + stringArg(FlutterOptions.kWebRendererFlag), + useWasm: useWasm + ); + @override Future runCommand() async { if (!globals.fs.isFileSync('pubspec.yaml')) { @@ -388,10 +393,6 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { ); } - final String? webRendererString = stringArg('web-renderer'); - final WebRendererMode webRenderer = (webRendererString != null) - ? WebRendererMode.values.byName(webRendererString) - : WebRendererMode.auto; final DebuggingOptions debuggingOptions = DebuggingOptions.enabled( buildInfo, startPaused: startPaused, @@ -405,6 +406,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { enableImpeller: ImpellerStatus.fromBool(argResults!['enable-impeller'] as bool?), debugLogsDirectoryPath: debugLogsDirectoryPath, webRenderer: webRenderer, + webUseWasm: useWasm, ); String? testAssetDirectory; @@ -511,6 +513,10 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { throwToolExit('--wasm is only supported on the web platform'); } + if (webRenderer == WebRendererMode.skwasm && !useWasm) { + throwToolExit('Skwasm renderer requires --wasm'); + } + Device? integrationTestDevice; if (_isIntegrationTest) { integrationTestDevice = await findTargetDevice(); @@ -589,7 +595,6 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { testAssetDirectory: testAssetDirectory, flutterProject: flutterProject, web: isWeb, - useWasm: useWasm, randomSeed: stringArg('test-randomize-ordering-seed'), reporter: stringArg('reporter'), fileReporter: stringArg('file-reporter'), diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 96006cdf4a..6afabbd5d0 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -994,6 +994,7 @@ class DebuggingOptions { this.webHeaders = const {}, this.webLaunchUrl, this.webRenderer = WebRendererMode.auto, + this.webUseWasm = false, this.vmserviceOutFile, this.fastStart = false, this.nullAssertions = false, @@ -1024,6 +1025,7 @@ class DebuggingOptions { this.webLaunchUrl, this.webHeaders = const {}, this.webRenderer = WebRendererMode.auto, + this.webUseWasm = false, this.cacheSkSL = false, this.traceAllowlist, this.enableImpeller = ImpellerStatus.platformDefault, @@ -1104,6 +1106,7 @@ class DebuggingOptions { required this.webHeaders, required this.webLaunchUrl, required this.webRenderer, + required this.webUseWasm, required this.vmserviceOutFile, required this.fastStart, required this.nullAssertions, @@ -1191,6 +1194,9 @@ class DebuggingOptions { /// Which web renderer to use for the debugging session final WebRendererMode webRenderer; + /// Whether to compile to webassembly + final bool webUseWasm; + /// A file where the VM Service URL should be written after the application is started. final String? vmserviceOutFile; final bool fastStart; @@ -1300,6 +1306,7 @@ class DebuggingOptions { 'webLaunchUrl': webLaunchUrl, 'webHeaders': webHeaders, 'webRenderer': webRenderer.name, + 'webUseWasm': webUseWasm, 'vmserviceOutFile': vmserviceOutFile, 'fastStart': fastStart, 'nullAssertions': nullAssertions, @@ -1356,6 +1363,7 @@ class DebuggingOptions { webHeaders: (json['webHeaders']! as Map).cast(), webLaunchUrl: json['webLaunchUrl'] as String?, webRenderer: WebRendererMode.values.byName(json['webRenderer']! as String), + webUseWasm: json['webUseWasm']! as bool, vmserviceOutFile: json['vmserviceOutFile'] as String?, fastStart: json['fastStart']! as bool, nullAssertions: json['nullAssertions']! 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 fa10cfb1cf..fd1e99c544 100644 --- a/packages/flutter_tools/lib/src/drive/web_driver_service.dart +++ b/packages/flutter_tools/lib/src/drive/web_driver_service.dart @@ -79,6 +79,7 @@ class WebDriverService extends DriverService { port: debuggingOptions.port, hostname: debuggingOptions.hostname, webRenderer: debuggingOptions.webRenderer, + webUseWasm: debuggingOptions.webUseWasm ) : DebuggingOptions.enabled( buildInfo, @@ -86,6 +87,7 @@ class WebDriverService extends DriverService { hostname: debuggingOptions.hostname, disablePortPublication: debuggingOptions.disablePortPublication, webRenderer: debuggingOptions.webRenderer, + webUseWasm: debuggingOptions.webUseWasm, ), stayResident: true, flutterProject: FlutterProject.current(), diff --git a/packages/flutter_tools/lib/src/isolated/devfs_web.dart b/packages/flutter_tools/lib/src/isolated/devfs_web.dart index a4a2c9f8d1..656c5f1729 100644 --- a/packages/flutter_tools/lib/src/isolated/devfs_web.dart +++ b/packages/flutter_tools/lib/src/isolated/devfs_web.dart @@ -180,6 +180,7 @@ class WebAssetServer implements AssetReader { Map extraHeaders, NullSafetyMode nullSafetyMode, { required WebRendererMode webRenderer, + required bool isWasm, bool testMode = false, DwdsLauncher dwdsLauncher = Dwds.start, // TODO(markzipan): Make sure this default value aligns with that in the debugger options. @@ -237,8 +238,8 @@ class WebAssetServer implements AssetReader { return server; } - // In release builds deploy a simpler proxy server. - if (buildInfo.mode != BuildMode.debug) { + // In release builds (or wasm builds) deploy a simpler proxy server. + if (buildInfo.mode != BuildMode.debug || isWasm) { final ReleaseAssetServer releaseAssetServer = ReleaseAssetServer( entrypoint, fileSystem: globals.fs, @@ -246,6 +247,7 @@ class WebAssetServer implements AssetReader { flutterRoot: Cache.flutterRoot, webBuildDirectory: getWebBuildDirectory(), basePath: server.basePath, + needsCoopCoep: webRenderer == WebRendererMode.skwasm, ); runZonedGuarded(() { shelf.serveRequests(httpServer!, releaseAssetServer.handle); @@ -737,6 +739,7 @@ class WebDevFS implements DevFS { required this.nullSafetyMode, required this.ddcModuleSystem, required this.webRenderer, + required this.isWasm, required this.rootDirectory, this.testMode = false, }) : _port = port; @@ -763,6 +766,7 @@ class WebDevFS implements DevFS { final String? tlsCertPath; final String? tlsCertKeyPath; final WebRendererMode webRenderer; + final bool isWasm; late WebAssetServer webAssetServer; @@ -863,6 +867,7 @@ class WebDevFS implements DevFS { extraHeaders, nullSafetyMode, webRenderer: webRenderer, + isWasm: isWasm, testMode: testMode, ddcModuleSystem: ddcModuleSystem, ); @@ -1108,11 +1113,13 @@ class ReleaseAssetServer { required String? webBuildDirectory, required String? flutterRoot, required Platform platform, + required bool needsCoopCoep, this.basePath = '', }) : _fileSystem = fileSystem, _platform = platform, _flutterRoot = flutterRoot, _webBuildDirectory = webBuildDirectory, + _needsCoopCoep = needsCoopCoep, _fileSystemUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform); @@ -1122,6 +1129,7 @@ class ReleaseAssetServer { final FileSystem _fileSystem; final FileSystemUtils _fileSystemUtils; final Platform _platform; + final bool _needsCoopCoep; /// The base path to serve from. /// @@ -1174,14 +1182,23 @@ class ReleaseAssetServer { 'application/octet-stream'; return shelf.Response.ok(bytes, headers: { 'Content-Type': mimeType, + if (_needsCoopCoep && file.basename == 'index.html') ...{ + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + } }); } final File file = _fileSystem .file(_fileSystem.path.join(_webBuildDirectory!, 'index.html')); - return shelf.Response.ok(file.readAsBytesSync(), headers: { - 'Content-Type': 'text/html', - }); + return shelf.Response.ok(file.readAsBytesSync(), + headers: { + 'Content-Type': 'text/html', + if (_needsCoopCoep) ...{ + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, + }); } } 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 62285209b4..a0de64f755 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -134,9 +134,9 @@ class ResidentWebRunner extends ResidentRunner { // and platform initialization. Directory? _generatedEntrypointDirectory; - // Only the debug builds of the web support the service protocol. + // Only non-wasm debug builds of the web support the service protocol. @override - bool get supportsServiceProtocol => isRunningDebug && deviceIsDebuggable; + bool get supportsServiceProtocol => !debuggingOptions.webUseWasm && isRunningDebug && deviceIsDebuggable; @override bool get debuggingEnabled => isRunningDebug && deviceIsDebuggable; @@ -311,13 +311,14 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). nativeNullAssertions: debuggingOptions.nativeNullAssertions, ddcModuleSystem: debuggingOptions.buildInfo.ddcModuleFormat == DdcModuleFormat.ddc, webRenderer: debuggingOptions.webRenderer, + isWasm: debuggingOptions.webUseWasm, rootDirectory: fileSystem.directory(projectRootPath), ); Uri url = await device!.devFS!.create(); if (debuggingOptions.tlsCertKeyPath != null && debuggingOptions.tlsCertPath != null) { url = url.replace(scheme: 'https'); } - if (debuggingOptions.buildInfo.isDebug) { + if (debuggingOptions.buildInfo.isDebug && !debuggingOptions.webUseWasm) { await runSourceGenerators(); final UpdateFSReport report = await _updateDevFS(fullRestart: true); if (!report.success) { @@ -342,12 +343,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). target, debuggingOptions.buildInfo, ServiceWorkerStrategy.none, - compilerConfigs: [ - JsCompilerConfig.run( - nativeNullAssertions: debuggingOptions.nativeNullAssertions, - renderer: debuggingOptions.webRenderer, - ) - ] + compilerConfigs: [_compilerConfig], ); } await device!.device!.startApp( @@ -386,6 +382,17 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). } } + WebCompilerConfig get _compilerConfig => (debuggingOptions.webUseWasm) + ? WasmCompilerConfig( + optimizationLevel: 0, + stripWasm: false, + renderer: debuggingOptions.webRenderer + ) + : JsCompilerConfig.run( + nativeNullAssertions: debuggingOptions.nativeNullAssertions, + renderer: debuggingOptions.webRenderer, + ); + @override Future restart({ bool fullRestart = false, @@ -399,7 +406,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). progressId: 'hot.restart', ); - if (debuggingOptions.buildInfo.isDebug) { + if (debuggingOptions.buildInfo.isDebug && !debuggingOptions.webUseWasm) { await runSourceGenerators(); // Full restart is always false for web, since the extra recompile is wasteful. final UpdateFSReport report = await _updateDevFS(); @@ -426,12 +433,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). target, debuggingOptions.buildInfo, ServiceWorkerStrategy.none, - compilerConfigs: [ - JsCompilerConfig.run( - nativeNullAssertions: debuggingOptions.nativeNullAssertions, - renderer: debuggingOptions.webRenderer, - ) - ], + compilerConfigs: [_compilerConfig], ); } on ToolExit { return OperationResult(1, 'Failed to recompile application.'); diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 87a2e513ec..addb06a6ff 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -1181,10 +1181,10 @@ abstract class ResidentRunner extends ResidentHandlers { bool get debuggingEnabled => debuggingOptions.debuggingEnabled; @override - bool get isRunningDebug => debuggingOptions.buildInfo.isDebug; + bool get isRunningDebug => !debuggingOptions.webUseWasm && debuggingOptions.buildInfo.isDebug; @override - bool get isRunningProfile => debuggingOptions.buildInfo.isProfile; + bool get isRunningProfile => !debuggingOptions.webUseWasm && debuggingOptions.buildInfo.isProfile; @override bool get isRunningRelease => debuggingOptions.buildInfo.isRelease; diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart index 9b85adbfac..bfa5c1de50 100644 --- a/packages/flutter_tools/lib/src/test/runner.dart +++ b/packages/flutter_tools/lib/src/test/runner.dart @@ -52,7 +52,6 @@ abstract class FlutterTestRunner { String? icudtlPath, Directory? coverageDirectory, bool web = false, - bool useWasm = false, String? randomSeed, String? reporter, String? fileReporter, @@ -118,7 +117,6 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { String? icudtlPath, Directory? coverageDirectory, bool web = false, - bool useWasm = false, String? randomSeed, String? reporter, String? fileReporter, @@ -188,7 +186,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { testFiles: testFiles.map((Uri uri) => uri.toFilePath()).toList(), buildInfo: debuggingOptions.buildInfo, webRenderer: debuggingOptions.webRenderer, - useWasm: useWasm, + useWasm: debuggingOptions.webUseWasm, ); testArgs ..add('--platform=chrome') @@ -221,7 +219,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { ), testTimeRecorder: testTimeRecorder, webRenderer: debuggingOptions.webRenderer, - useWasm: useWasm, + useWasm: debuggingOptions.webUseWasm, ); }, ); diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart index 22302d05bb..66a99ad85b 100644 --- a/packages/flutter_tools/lib/src/web/compile.dart +++ b/packages/flutter_tools/lib/src/web/compile.dart @@ -184,6 +184,17 @@ enum WebRendererMode implements CliEnum { /// Always use skwasm. skwasm; + factory WebRendererMode.fromCliOption(String? webRendererString, {required bool useWasm}) { + final WebRendererMode mode = webRendererString != null + ? WebRendererMode.values.byName(webRendererString) + : WebRendererMode.auto; + if (mode == WebRendererMode.auto && useWasm) { + // Wasm defaults to skwasm + return WebRendererMode.skwasm; + } + return mode; + } + @override String get cliName => snakeCase(name, '-'); diff --git a/packages/flutter_tools/lib/src/web/compiler_config.dart b/packages/flutter_tools/lib/src/web/compiler_config.dart index 79206ad238..352baa5b0e 100644 --- a/packages/flutter_tools/lib/src/web/compiler_config.dart +++ b/packages/flutter_tools/lib/src/web/compiler_config.dart @@ -22,7 +22,7 @@ sealed class WebCompilerConfig { /// The compiler optimization level. /// - /// Valid values are O1 (lowest, profile default) to O4 (highest, release default). + /// Valid values are O0 (lowest, debug default) to O4 (highest, release default). final int optimizationLevel; /// Returns which target this compiler outputs (js or wasm) 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 d62cafd47d..4d74c30b3a 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart @@ -29,6 +29,7 @@ import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:flutter_tools/src/vmservice.dart'; +import 'package:flutter_tools/src/web/compile.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart' as analytics; import 'package:vm_service/vm_service.dart'; @@ -986,6 +987,36 @@ void main() { DeviceManager: () => testDeviceManager, }); + testUsingContext('throws a ToolExit when using --wasm on a non-web platform', () async { + final RunCommand command = RunCommand(); + await expectLater( + () => createTestCommandRunner(command).run([ + 'run', + '--no-pub', + '--wasm', + ]), throwsToolExit(message: '--wasm is only supported on the web platform')); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + Logger: () => logger, + DeviceManager: () => testDeviceManager, + }); + + testUsingContext('throws a ToolExit when using the skwasm renderer without --wasm', () async { + final RunCommand command = RunCommand(); + await expectLater( + () => createTestCommandRunner(command).run([ + 'run', + '--no-pub', + '--web-renderer=skwasm', + ]), throwsToolExit(message: 'Skwasm renderer requires --wasm')); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + Logger: () => logger, + DeviceManager: () => testDeviceManager, + }); + testUsingContext('accepts headers with commas in them', () async { final RunCommand command = RunCommand(); await expectLater( @@ -1170,6 +1201,24 @@ void main() { ProcessManager: () => FakeProcessManager.any(), }); + testUsingContext('wasm mode selects skwasm renderer by default', () async { + final RunCommand command = RunCommand(); + await expectLater(() => createTestCommandRunner(command).run([ + 'run', + '-d chrome', + '--wasm', + ]), throwsToolExit()); + + final DebuggingOptions options = await command.createDebuggingOptions(false); + + expect(options.webUseWasm, true); + expect(options.webRenderer, WebRendererMode.skwasm); + }, overrides: { + Cache: () => Cache.test(processManager: FakeProcessManager.any()), + FileSystem: () => MemoryFileSystem.test(), + ProcessManager: () => FakeProcessManager.any(), + }); + testUsingContext('fails when "--web-launch-url" is not supported', () async { final RunCommand command = RunCommand(); await expectLater( diff --git a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart index fa27e70499..7ddef36ef6 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart @@ -1348,6 +1348,24 @@ dev_dependencies: FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); + + testUsingContext('Web renderer defaults to Skwasm when using wasm', () async { + final FakeFlutterTestRunner testRunner = FakeFlutterTestRunner(0); + + final TestCommand testCommand = TestCommand(testRunner: testRunner); + final CommandRunner commandRunner = createTestCommandRunner(testCommand); + + await commandRunner.run(const [ + 'test', + '--no-pub', + '--platform=chrome', + '--wasm', + ]); + expect(testRunner.lastDebuggingOptionsValue.webRenderer, WebRendererMode.skwasm); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + }); }); } 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 bcb9e247a0..2b18e138b0 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 @@ -137,6 +137,7 @@ void main() { flutterRoot: null, // ignore: avoid_redundant_argument_values platform: FakePlatform(), webBuildDirectory: null, // ignore: avoid_redundant_argument_values + needsCoopCoep: false, ); }, overrides: { @@ -927,6 +928,7 @@ void main() { nullSafetyMode: NullSafetyMode.unsound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.html, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); @@ -1063,6 +1065,7 @@ void main() { nullSafetyMode: NullSafetyMode.sound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.html, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); @@ -1199,6 +1202,7 @@ void main() { nullSafetyMode: NullSafetyMode.sound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.canvaskit, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); @@ -1272,6 +1276,7 @@ void main() { nullSafetyMode: NullSafetyMode.sound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.canvaskit, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); @@ -1321,6 +1326,7 @@ void main() { nullSafetyMode: NullSafetyMode.sound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.canvaskit, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); @@ -1372,6 +1378,7 @@ void main() { nullSafetyMode: NullSafetyMode.sound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.auto, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); @@ -1425,6 +1432,7 @@ void main() { nullSafetyMode: NullSafetyMode.unsound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.canvaskit, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); @@ -1463,6 +1471,7 @@ void main() { const {}, NullSafetyMode.unsound, webRenderer: WebRendererMode.canvaskit, + isWasm: false, testMode: true); expect(webAssetServer.defaultResponseHeaders['x-frame-options'], null); @@ -1496,6 +1505,7 @@ void main() { }, NullSafetyMode.unsound, webRenderer: WebRendererMode.canvaskit, + isWasm: false, testMode: true); expect(webAssetServer.defaultResponseHeaders[extraHeaderKey], @@ -1596,6 +1606,7 @@ void main() { nullSafetyMode: NullSafetyMode.unsound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.canvaskit, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.ddcModuleLoaderJS.createSync(recursive: true); 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 7802b1d148..c4074f54ae 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 @@ -76,6 +76,7 @@ void main() { flutterRoot: null, platform: FakePlatform(), webBuildDirectory: null, + needsCoopCoep: false, ); }, overrides: { Logger: () => logger, @@ -715,6 +716,7 @@ void main() { nullSafetyMode: NullSafetyMode.unsound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.html, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.requireJS.createSync(recursive: true); @@ -827,6 +829,7 @@ void main() { nullSafetyMode: NullSafetyMode.sound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.html, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.requireJS.createSync(recursive: true); @@ -945,6 +948,7 @@ void main() { nullSafetyMode: NullSafetyMode.sound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.canvaskit, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.requireJS.createSync(recursive: true); @@ -1009,6 +1013,7 @@ void main() { nullSafetyMode: NullSafetyMode.sound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.canvaskit, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.requireJS.createSync(recursive: true); @@ -1057,6 +1062,7 @@ void main() { nullSafetyMode: NullSafetyMode.sound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.canvaskit, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.requireJS.createSync(recursive: true); @@ -1106,6 +1112,7 @@ void main() { nullSafetyMode: NullSafetyMode.sound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.auto, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.requireJS.createSync(recursive: true); @@ -1156,6 +1163,7 @@ void main() { nullSafetyMode: NullSafetyMode.unsound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.canvaskit, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.requireJS.createSync(recursive: true); @@ -1194,6 +1202,7 @@ void main() { const {}, NullSafetyMode.unsound, webRenderer: WebRendererMode.canvaskit, + isWasm: false, testMode: true ); @@ -1228,6 +1237,7 @@ void main() { }, NullSafetyMode.unsound, webRenderer: WebRendererMode.canvaskit, + isWasm: false, testMode: true ); @@ -1310,6 +1320,7 @@ void main() { nullSafetyMode: NullSafetyMode.unsound, ddcModuleSystem: usesDdcModuleSystem, webRenderer: WebRendererMode.canvaskit, + isWasm: false, rootDirectory: globals.fs.currentDirectory, ); webDevFS.requireJS.createSync(recursive: true); diff --git a/packages/flutter_tools/test/general.shard/web/web_asset_server_test.dart b/packages/flutter_tools/test/general.shard/web/web_asset_server_test.dart index 858ed1126d..d0b819600c 100644 --- a/packages/flutter_tools/test/general.shard/web/web_asset_server_test.dart +++ b/packages/flutter_tools/test/general.shard/web/web_asset_server_test.dart @@ -49,6 +49,7 @@ void main() { platform: platform, flutterRoot: '/flutter', webBuildDirectory: 'build/web', + needsCoopCoep: false, ); fileSystem.file('build/web/assets/foo.png') ..createSync(recursive: true) @@ -68,6 +69,7 @@ void main() { platform: platform, flutterRoot: '/flutter', webBuildDirectory: 'build/web', + needsCoopCoep: false, ); fileSystem.file('build/web/assets/foo.js') ..createSync(recursive: true) @@ -87,6 +89,7 @@ void main() { platform: platform, flutterRoot: '/flutter', webBuildDirectory: 'build/web', + needsCoopCoep: false, ); fileSystem.file('build/web/assets/foo.html') ..createSync(recursive: true) @@ -106,6 +109,7 @@ void main() { platform: platform, flutterRoot: '/flutter', webBuildDirectory: 'build/web', + needsCoopCoep: false, ); fileSystem.file('flutter/bar.dart') ..createSync(recursive: true) @@ -122,6 +126,7 @@ void main() { platform: platform, flutterRoot: '/flutter', webBuildDirectory: 'build/web', + needsCoopCoep: false, ); fileSystem.file('bar.dart') ..createSync(recursive: true) @@ -131,4 +136,44 @@ void main() { expect(response.statusCode, HttpStatus.ok); }); + + testWithoutContext('release asset server serves html content with COOP/COEP headers when specified', () async { + final ReleaseAssetServer assetServer = ReleaseAssetServer(Uri.base, + fileSystem: fileSystem, + platform: platform, + flutterRoot: '/flutter', + webBuildDirectory: 'build/web', + needsCoopCoep: true, + ); + fileSystem.file('build/web/index.html') + ..createSync(recursive: true) + ..writeAsStringSync(''); + final Response response = await assetServer + .handle(Request('GET', Uri.parse('http://localhost:8080/index.html'))); + + expect(response.statusCode, HttpStatus.ok); + final Map headers = response.headers; + expect(headers['Cross-Origin-Opener-Policy'], 'same-origin'); + expect(headers['Cross-Origin-Embedder-Policy'], 'require-corp'); + }); + + testWithoutContext('release asset server serves html content without COOP/COEP headers when specified', () async { + final ReleaseAssetServer assetServer = ReleaseAssetServer(Uri.base, + fileSystem: fileSystem, + platform: platform, + flutterRoot: '/flutter', + webBuildDirectory: 'build/web', + needsCoopCoep: false, + ); + fileSystem.file('build/web/index.html') + ..createSync(recursive: true) + ..writeAsStringSync(''); + final Response response = await assetServer + .handle(Request('GET', Uri.parse('http://localhost:8080/index.html'))); + + expect(response.statusCode, HttpStatus.ok); + final Map headers = response.headers; + expect(headers.containsKey('Cross-Origin-Opener-Policy'), false); + expect(headers.containsKey('Cross-Origin-Embedder-Policy'), false); + }); } diff --git a/packages/integration_test/example/web/flutter_bootstrap.js b/packages/integration_test/example/web/flutter_bootstrap.js new file mode 100644 index 0000000000..be78d93ce8 --- /dev/null +++ b/packages/integration_test/example/web/flutter_bootstrap.js @@ -0,0 +1,12 @@ +// 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. + +{{flutter_js}} +{{flutter_build_config}} +_flutter.loader.load({ + config: { + // Use the local CanvasKit bundle instead of the CDN to reduce test flakiness. + canvasKitBaseUrl: "/canvaskit/", + }, +}); diff --git a/packages/integration_test/example/web/index.html b/packages/integration_test/example/web/index.html index fe77e24360..c02e33b3ae 100644 --- a/packages/integration_test/example/web/index.html +++ b/packages/integration_test/example/web/index.html @@ -21,16 +21,6 @@ found in the LICENSE file. --> - - - +