blob: 7b8f819b90ef6c8d54e09d62f229f9cf0be148bf [file] [log] [blame]
// 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:dwds/dwds.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import '../artifacts.dart';
import '../asset.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/net.dart';
import '../base/platform.dart';
import '../build_info.dart';
import '../build_system/tools/shader_compiler.dart';
import '../bundle_builder.dart';
import '../compile.dart';
import '../devfs.dart';
import '../device.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../vmservice.dart';
import '../web/bootstrap.dart';
import '../web/chrome.dart';
import '../web/compile.dart';
import '../web/devfs_config.dart';
import '../web_template.dart';
import 'web_asset_server.dart';
const kLuciEnvName = 'LUCI_CONTEXT';
/// A web server which handles serving JavaScript and assets.
///
/// This is only used in development mode.
class ConnectionResult {
ConnectionResult(this.appConnection, this.debugConnection, this.vmService);
final AppConnection? appConnection;
final DebugConnection? debugConnection;
final vm_service.VmService vmService;
}
typedef VmServiceFactory =
Future<vm_service.VmService> Function(
Uri, {
CompressionOptions compression,
required Logger logger,
});
/// The web specific DevFS implementation.
class WebDevFS implements DevFS {
/// Create a new [WebDevFS] instance.
///
/// [testMode] is true, do not actually initialize dwds or the shelf static
/// server.
WebDevFS({
required this.packagesFilePath,
required this.urlTunneller,
required this.useSseForDebugProxy,
required this.useSseForDebugBackend,
required this.useSseForInjectedClient,
required this.buildInfo,
required this.enableDwds,
required this.enableDds,
this.ddsPort,
required this.entrypoint,
required this.expressionCompiler,
required this.chromiumLauncher,
required this.nativeNullAssertions,
required this.ddcModuleSystem,
required this.canaryFeatures,
required this.webDevServerConfig,
required this.webRenderer,
required this.isWasm,
required this.useLocalCanvasKit,
required this.rootDirectory,
this.useDwdsWebSocketConnection = false,
required this.fileSystem,
required this.logger,
required this.platform,
this.testMode = false,
}) {
// TODO(srujzs): Remove this assertion when the library bundle format is
// supported without canary mode.
if (ddcModuleSystem) {
assert(canaryFeatures);
}
}
final Uri entrypoint;
final String packagesFilePath;
final UrlTunneller? urlTunneller;
final bool useSseForDebugProxy;
final bool useSseForDebugBackend;
final bool useSseForInjectedClient;
final BuildInfo buildInfo;
final bool enableDwds;
final bool enableDds;
final int? ddsPort;
final bool testMode;
final bool ddcModuleSystem;
final bool canaryFeatures;
final ExpressionCompiler? expressionCompiler;
final ChromiumLauncher? chromiumLauncher;
final bool nativeNullAssertions;
final WebRendererMode webRenderer;
final bool isWasm;
final bool useLocalCanvasKit;
final WebDevServerConfig webDevServerConfig;
final bool useDwdsWebSocketConnection;
final FileSystem fileSystem;
final Logger logger;
final Platform platform;
late WebAssetServer webAssetServer;
Dwds get dwds => webAssetServer.dwds;
/// Whether middleware should be enabled for this web development server.
/// Middleware is enabled when using Chrome device or DDC module system.
bool get shouldEnableMiddleware => chromiumLauncher != null || ddcModuleSystem;
// A flag to indicate whether we have called `setAssetDirectory` on the target device.
@override
var hasSetAssetDirectory = false;
@override
var didUpdateFontManifest = false;
Future<DebugConnection>? _cachedExtensionFuture;
StreamSubscription<void>? _connectedApps;
/// Connect and retrieve the [DebugConnection] for the current application.
///
/// Only calls [AppConnection.runMain] on the subsequent connections. This
/// should be called before the browser is launched to make sure the listener
/// is registered early enough.
Future<ConnectionResult?> connect(
bool useDebugExtension, {
@visibleForTesting VmServiceFactory vmServiceFactory = createVmServiceDelegate,
}) {
final firstConnection = Completer<ConnectionResult>();
// Note there is an asynchronous gap between this being set to true and
// [firstConnection] completing; thus test the boolean to determine if
// the current connection is the first.
var foundFirstConnection = false;
_connectedApps = dwds.connectedApps.listen(
(AppConnection appConnection) async {
try {
final DebugConnection debugConnection = useDebugExtension
? await (_cachedExtensionFuture ??= dwds.extensionDebugConnections.stream.first)
: await dwds.debugConnection(appConnection);
if (foundFirstConnection) {
appConnection.runMain();
} else {
foundFirstConnection = true;
final vm_service.VmService vmService = await vmServiceFactory(
Uri.parse(debugConnection.uri),
logger: logger,
);
firstConnection.complete(ConnectionResult(appConnection, debugConnection, vmService));
}
} on Exception catch (error, stackTrace) {
if (!firstConnection.isCompleted) {
firstConnection.completeError(error, stackTrace);
}
}
},
onError: (Object error, StackTrace stackTrace) {
logger.printError('Unknown error while waiting for debug connection:$error\n$stackTrace');
if (!firstConnection.isCompleted) {
firstConnection.completeError(error, stackTrace);
}
},
);
return firstConnection.future;
}
@override
var sources = <Uri>[];
@override
DateTime? lastCompiled;
@override
PackageConfig? lastPackageConfig;
// We do not evict assets on the web.
@override
Set<String> get assetPathsToEvict => const <String>{};
@override
Uri get baseUri => webAssetServer.baseUri;
@override
Future<Uri> create() async {
webAssetServer = await WebAssetServer.start(
chromiumLauncher,
urlTunneller,
useSseForDebugProxy,
useSseForDebugBackend,
useSseForInjectedClient,
buildInfo,
enableDwds,
enableDds,
ddsPort,
entrypoint,
expressionCompiler,
webRenderer: webRenderer,
isWasm: isWasm,
useLocalCanvasKit: useLocalCanvasKit,
testMode: testMode,
ddcModuleSystem: ddcModuleSystem,
canaryFeatures: canaryFeatures,
webDevServerConfig: webDevServerConfig,
useDwdsWebSocketConnection: useDwdsWebSocketConnection,
fileSystem: fileSystem,
logger: logger,
platform: platform,
shouldEnableMiddleware: shouldEnableMiddleware,
);
return baseUri;
}
@override
Future<void> destroy() async {
await webAssetServer.dispose();
await _connectedApps?.cancel();
}
@override
Uri deviceUriToHostUri(Uri deviceUri) {
return deviceUri;
}
@override
String get fsName => 'web_asset';
@override
final Directory rootDirectory;
Future<void> _validateTemplateFile(String filename) async {
final File file = fileSystem.currentDirectory.childDirectory('web').childFile(filename);
if (!await file.exists()) {
return;
}
final template = WebTemplate(await file.readAsString());
for (final WebTemplateWarning warning in template.getWarnings()) {
logger.printWarning('Warning: In $filename:${warning.lineNumber}: ${warning.warningText}');
}
}
@override
Future<UpdateFSReport> update({
required Uri mainUri,
required ResidentCompiler generator,
required bool trackWidgetCreation,
required String pathToReload,
required List<Uri> invalidatedFiles,
required PackageConfig packageConfig,
required String dillOutputPath,
required DevelopmentShaderCompiler shaderCompiler,
DevFSWriter? devFSWriter,
String? target,
AssetBundle? bundle,
bool bundleFirstUpload = false,
bool fullRestart = false,
bool resetCompiler = false,
String? projectRootPath,
File? dartPluginRegistrant,
}) async {
lastPackageConfig = packageConfig;
final File mainFile = fileSystem.file(mainUri);
final String outputDirectoryPath = mainFile.parent.path;
if (bundleFirstUpload) {
webAssetServer.entrypointCacheDirectory = fileSystem.directory(outputDirectoryPath);
generator.addFileSystemRoot(outputDirectoryPath);
final String entrypoint = fileSystem.path.basename(mainFile.path);
webAssetServer.writeBytes(entrypoint, mainFile.readAsBytesSync());
if (ddcModuleSystem) {
webAssetServer.writeBytes('ddc_module_loader.js', ddcModuleLoaderJS.readAsBytesSync());
} else {
webAssetServer.writeBytes('require.js', requireJS.readAsBytesSync());
}
webAssetServer.writeBytes('flutter.js', flutterJs.readAsBytesSync());
webAssetServer.writeBytes('stack_trace_mapper.js', stackTraceMapper.readAsBytesSync());
webAssetServer.writeFile('manifest.json', '{"info":"manifest not generated in run mode."}');
webAssetServer.writeFile(
'flutter_service_worker.js',
'// Service worker not loaded in run mode.',
);
webAssetServer.writeFile('version.json', FlutterProject.current().getVersionInfo());
webAssetServer.writeFile(
'main.dart.js',
ddcModuleSystem
? generateDDCLibraryBundleBootstrapScript(
entrypoint: entrypoint,
ddcModuleLoaderUrl: 'ddc_module_loader.js',
mapperUrl: 'stack_trace_mapper.js',
generateLoadingIndicator: shouldEnableMiddleware,
isWindows: platform.isWindows,
)
: generateBootstrapScript(
requireUrl: 'require.js',
mapperUrl: 'stack_trace_mapper.js',
generateLoadingIndicator: shouldEnableMiddleware,
),
);
const onLoadEndBootstrap = 'on_load_end_bootstrap.js';
if (ddcModuleSystem) {
webAssetServer.writeFile(onLoadEndBootstrap, generateDDCLibraryBundleOnLoadEndBootstrap());
}
webAssetServer.writeFile(
'main_module.bootstrap.js',
ddcModuleSystem
? generateDDCLibraryBundleMainModule(
entrypoint: entrypoint,
nativeNullAssertions: nativeNullAssertions,
onLoadEndBootstrap: onLoadEndBootstrap,
isCi: platform.environment.containsKey(kLuciEnvName),
)
: generateMainModule(
entrypoint: entrypoint,
nativeNullAssertions: nativeNullAssertions,
loaderRootDirectory: baseUri.toString(),
),
);
// TODO(zanderso): refactor the asset code in this and the regular devfs to
// be shared.
if (bundle != null) {
await writeBundle(
fileSystem.directory(getAssetBuildDirectory()),
bundle.entries,
targetPlatform: TargetPlatform.web_javascript,
impellerStatus: ImpellerStatus.disabled,
processManager: globals.processManager,
fileSystem: fileSystem,
artifacts: globals.artifacts!,
logger: logger,
projectDir: rootDirectory,
buildMode: buildInfo.mode,
);
}
}
await _validateTemplateFile('index.html');
await _validateTemplateFile('flutter_bootstrap.js');
final candidateCompileTime = DateTime.now();
if (resetCompiler) {
generator.reset();
}
// The tool generates an entrypoint file in a temp directory to handle
// the web specific bootstrap logic. To make it easier for DWDS to handle
// mapping the file name, this is done via an additional file root and
// special hard-coded scheme.
final CompilerOutput? compilerOutput = await generator.recompile(
Uri(scheme: 'org-dartlang-app', path: '/${mainUri.pathSegments.last}'),
invalidatedFiles,
outputPath: dillOutputPath,
packageConfig: packageConfig,
projectRootPath: projectRootPath,
fs: fileSystem,
dartPluginRegistrant: dartPluginRegistrant,
recompileRestart: fullRestart,
);
if (compilerOutput == null || compilerOutput.errorCount > 0) {
return UpdateFSReport(
// TODO(srujzs): We're currently reliant on compile error string parsing
// as hot reload rejections are sent to stderr just like other
// compilation errors. Ideally, we should have some shared parsing
// functionality, but that would require a shared package.
// See https://github.com/dart-lang/sdk/issues/60275.
hotReloadRejected: compilerOutput?.errorMessage?.contains('Hot reload rejected') ?? false,
);
}
// Only update the last compiled time if we successfully compiled.
lastCompiled = candidateCompileTime;
// list of sources that needs to be monitored are in [compilerOutput.sources]
sources = compilerOutput.sources;
late File codeFile;
File manifestFile;
File sourcemapFile;
File metadataFile;
late List<String> modules;
try {
final Directory parentDirectory = fileSystem.directory(outputDirectoryPath);
codeFile = parentDirectory.childFile('${compilerOutput.outputFilename}.sources');
manifestFile = parentDirectory.childFile('${compilerOutput.outputFilename}.json');
sourcemapFile = parentDirectory.childFile('${compilerOutput.outputFilename}.map');
metadataFile = parentDirectory.childFile('${compilerOutput.outputFilename}.metadata');
modules = webAssetServer.webMemoryFS.write(
codeFile,
manifestFile,
sourcemapFile,
metadataFile,
);
} on FileSystemException catch (err) {
throwToolExit('Failed to load recompiled sources:\n$err');
}
webAssetServer.updateModulesAndDigests(modules);
if (!bundleFirstUpload && ddcModuleSystem) {
webAssetServer.writeReloadedSources(modules);
}
return UpdateFSReport(
success: true,
syncedBytes: codeFile.lengthSync(),
invalidatedSourcesCount: invalidatedFiles.length,
);
}
@visibleForTesting
File get requireJS => fileSystem.file(
fileSystem.path.join(
globals.artifacts!.getArtifactPath(
Artifact.engineDartSdkPath,
platform: TargetPlatform.web_javascript,
),
'lib',
'dev_compiler',
'amd',
'require.js',
),
);
@visibleForTesting
File get ddcModuleLoaderJS => fileSystem.file(
fileSystem.path.join(
globals.artifacts!.getArtifactPath(
Artifact.engineDartSdkPath,
platform: TargetPlatform.web_javascript,
),
'lib',
'dev_compiler',
'ddc',
'ddc_module_loader.js',
),
);
@visibleForTesting
File get flutterJs => fileSystem.file(
fileSystem.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
),
);
@visibleForTesting
File get stackTraceMapper => fileSystem.file(
fileSystem.path.join(
globals.artifacts!.getArtifactPath(
Artifact.engineDartSdkPath,
platform: TargetPlatform.web_javascript,
),
'lib',
'dev_compiler',
'web',
'dart_stack_trace_mapper.js',
),
);
@override
void resetLastCompiled() {
// Not used for web compilation.
}
@override
Set<String> get shaderPathsToEvict => <String>{};
}