// 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:crypto/crypto.dart'; import '../../artifacts.dart'; import '../../base/file_system.dart'; import '../../base/io.dart'; import '../../build_info.dart'; import '../../compile.dart'; import '../../dart/package_map.dart'; import '../../globals.dart' as globals; import '../build_system.dart'; import '../depfile.dart'; import 'assets.dart'; import 'dart.dart'; /// Whether web builds should call the platform initialization logic. const String kInitializePlatform = 'InitializePlatform'; /// Whether the application has web plugins. const String kHasWebPlugins = 'HasWebPlugins'; /// An override for the dart2js build mode. /// /// Valid values are O1 (lowest, profile default) to O4 (highest, release default). const String kDart2jsOptimization = 'Dart2jsOptimization'; /// Whether to disable dynamic generation code to satisfy csp policies. const String kCspMode = 'cspMode'; /// Generates an entry point for a web target. // Keep this in sync with build_runner/resident_web_runner.dart class WebEntrypointTarget extends Target { const WebEntrypointTarget(); @override String get name => 'web_entrypoint'; @override List<Target> get dependencies => const <Target>[]; @override List<Source> get inputs => const <Source>[ Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/web.dart'), ]; @override List<Source> get outputs => const <Source>[ Source.pattern('{BUILD_DIR}/main.dart'), ]; @override Future<void> build(Environment environment) async { final String targetFile = environment.defines[kTargetFile]; final bool shouldInitializePlatform = environment.defines[kInitializePlatform] == 'true'; final bool hasPlugins = environment.defines[kHasWebPlugins] == 'true'; final String importPath = globals.fs.path.absolute(targetFile); // Use the package uri mapper to find the correct package-scheme import path // for the user application. If the application has a mix of package-scheme // and relative imports for a library, then importing the entrypoint as a // file-scheme will cause said library to be recognized as two distinct // libraries. This can cause surprising behavior as types from that library // will be considered distinct from each other. final PackageUriMapper packageUriMapper = PackageUriMapper( importPath, PackageMap.globalPackagesPath, null, null, ); // By construction, this will only be null if the .packages file does not // have an entry for the user's application or if the main file is // outside of the lib/ directory. final String mainImport = packageUriMapper.map(importPath)?.toString() ?? globals.fs.file(importPath).absolute.uri.toString(); String contents; if (hasPlugins) { final String generatedPath = environment.projectDir .childDirectory('lib') .childFile('generated_plugin_registrant.dart') .absolute.path; final String generatedImport = packageUriMapper.map(generatedPath)?.toString() ?? globals.fs.file(generatedPath).absolute.uri.toString(); contents = ''' import 'dart:ui' as ui; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import '$generatedImport'; import '$mainImport' as entrypoint; Future<void> main() async { registerPlugins(webPluginRegistry); if ($shouldInitializePlatform) { await ui.webOnlyInitializePlatform(); } entrypoint.main(); } '''; } else { contents = ''' import 'dart:ui' as ui; import '$mainImport' as entrypoint; Future<void> main() async { if ($shouldInitializePlatform) { await ui.webOnlyInitializePlatform(); } entrypoint.main(); } '''; } environment.buildDir.childFile('main.dart') .writeAsStringSync(contents); } } /// Compiles a web entry point with dart2js. class Dart2JSTarget extends Target { const Dart2JSTarget(); @override String get name => 'dart2js'; @override List<Target> get dependencies => const <Target>[ WebEntrypointTarget() ]; @override List<Source> get inputs => const <Source>[ Source.artifact(Artifact.flutterWebSdk), Source.artifact(Artifact.dart2jsSnapshot), Source.artifact(Artifact.engineDartBinary), Source.pattern('{BUILD_DIR}/main.dart'), Source.pattern('{PROJECT_DIR}/.packages'), ]; @override List<Source> get outputs => const <Source>[]; @override List<String> get depfiles => const <String>[ 'dart2js.d', ]; @override Future<void> build(Environment environment) async { final String dart2jsOptimization = environment.defines[kDart2jsOptimization]; final bool csp = environment.defines[kCspMode] == 'true'; final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); final String specPath = globals.fs.path.join(globals.artifacts.getArtifactPath(Artifact.flutterWebSdk), 'libraries.json'); final String packageFile = PackageMap.globalPackagesPath; final File outputKernel = environment.buildDir.childFile('app.dill'); final File outputFile = environment.buildDir.childFile('main.dart.js'); final List<String> dartDefines = parseDartDefines(environment); // Run the dart2js compilation in two stages, so that icon tree shaking can // parse the kernel file for web builds. final ProcessResult kernelResult = await globals.processManager.run(<String>[ globals.artifacts.getArtifactPath(Artifact.engineDartBinary), globals.artifacts.getArtifactPath(Artifact.dart2jsSnapshot), '--libraries-spec=$specPath', '-o', outputKernel.path, '--packages=$packageFile', '--cfe-only', environment.buildDir.childFile('main.dart').path, ]); if (kernelResult.exitCode != 0) { throw Exception(kernelResult.stdout + kernelResult.stderr); } final ProcessResult javaScriptResult = await globals.processManager.run(<String>[ globals.artifacts.getArtifactPath(Artifact.engineDartBinary), globals.artifacts.getArtifactPath(Artifact.dart2jsSnapshot), '--libraries-spec=$specPath', if (dart2jsOptimization != null) '-$dart2jsOptimization' else '-O4', if (buildMode == BuildMode.profile) '-Ddart.vm.profile=true' else '-Ddart.vm.product=true', for (final String dartDefine in dartDefines) '-D$dartDefine', if (buildMode == BuildMode.profile) '--no-minify', if (csp) '--csp', '-o', outputFile.path, environment.buildDir.childFile('app.dill').path, ]); if (javaScriptResult.exitCode != 0) { throw Exception(javaScriptResult.stdout + javaScriptResult.stderr); } final File dart2jsDeps = environment.buildDir .childFile('app.dill.deps'); if (!dart2jsDeps.existsSync()) { globals.printError('Warning: dart2js did not produced expected deps list at ' '${dart2jsDeps.path}'); return; } final DepfileService depfileService = DepfileService( fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, ); final Depfile depfile = depfileService.parseDart2js( environment.buildDir.childFile('app.dill.deps'), outputFile, ); depfileService.writeToFile( depfile, environment.buildDir.childFile('dart2js.d'), ); } } /// Unpacks the dart2js compilation and resources to a given output directory class WebReleaseBundle extends Target { const WebReleaseBundle(); @override String get name => 'web_release_bundle'; @override List<Target> get dependencies => const <Target>[ Dart2JSTarget(), ]; @override List<Source> get inputs => const <Source>[ Source.pattern('{BUILD_DIR}/main.dart.js'), Source.pattern('{PROJECT_DIR}/pubspec.yaml'), ]; @override List<Source> get outputs => const <Source>[ Source.pattern('{OUTPUT_DIR}/main.dart.js'), ]; @override List<String> get depfiles => const <String>[ 'dart2js.d', 'flutter_assets.d', 'web_resources.d', ]; @override Future<void> build(Environment environment) async { for (final File outputFile in environment.buildDir.listSync(recursive: true).whereType<File>()) { final String basename = globals.fs.path.basename(outputFile.path); if (!basename.contains('main.dart.js')) { continue; } // Do not copy the deps file. if (basename.endsWith('.deps')) { continue; } outputFile.copySync( environment.outputDir.childFile(globals.fs.path.basename(outputFile.path)).path ); } final Directory outputDirectory = environment.outputDir.childDirectory('assets'); outputDirectory.createSync(recursive: true); final Depfile depfile = await copyAssets(environment, environment.outputDir.childDirectory('assets')); final DepfileService depfileService = DepfileService( fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, ); depfileService.writeToFile( depfile, environment.buildDir.childFile('flutter_assets.d'), ); final Directory webResources = environment.projectDir .childDirectory('web'); final List<File> inputResourceFiles = webResources .listSync(recursive: true) .whereType<File>() .toList(); // Copy other resource files out of web/ directory. final List<File> outputResourcesFiles = <File>[]; for (final File inputFile in inputResourceFiles) { final File outputFile = globals.fs.file(globals.fs.path.join( environment.outputDir.path, globals.fs.path.relative(inputFile.path, from: webResources.path))); if (!outputFile.parent.existsSync()) { outputFile.parent.createSync(recursive: true); } inputFile.copySync(outputFile.path); outputResourcesFiles.add(outputFile); } final Depfile resourceFile = Depfile(inputResourceFiles, outputResourcesFiles); depfileService.writeToFile( resourceFile, environment.buildDir.childFile('web_resources.d'), ); } } /// Generate a service worker for a web target. class WebServiceWorker extends Target { const WebServiceWorker(); @override String get name => 'web_service_worker'; @override List<Target> get dependencies => const <Target>[ Dart2JSTarget(), WebReleaseBundle(), ]; @override List<String> get depfiles => const <String>[ 'service_worker.d', ]; @override List<Source> get inputs => const <Source>[]; @override List<Source> get outputs => const <Source>[]; @override Future<void> build(Environment environment) async { final List<File> contents = environment.outputDir .listSync(recursive: true) .whereType<File>() .where((File file) => !file.path.endsWith('flutter_service_worker.js') && !globals.fs.path.basename(file.path).startsWith('.')) .toList(); final Map<String, String> urlToHash = <String, String>{}; for (final File file in contents) { // Do not force caching of source maps. if (file.path.endsWith('main.dart.js.map')) { continue; } final String url = globals.fs.path.toUri( globals.fs.path.relative( file.path, from: environment.outputDir.path), ).toString(); final String hash = md5.convert(await file.readAsBytes()).toString(); urlToHash[url] = hash; // Add an additional entry for the base URL. if (globals.fs.path.basename(url) == 'index.html') { urlToHash['/'] = hash; } } final File serviceWorkerFile = environment.outputDir .childFile('flutter_service_worker.js'); final Depfile depfile = Depfile(contents, <File>[serviceWorkerFile]); final String serviceWorker = generateServiceWorker(urlToHash); serviceWorkerFile .writeAsStringSync(serviceWorker); final DepfileService depfileService = DepfileService( fileSystem: globals.fs, logger: globals.logger, platform: globals.platform, ); depfileService.writeToFile( depfile, environment.buildDir.childFile('service_worker.d'), ); } } /// Generate a service worker with an app-specific cache name a map of /// resource files. /// /// We embed file hashes directly into the worker so that the byte for byte /// invalidation will automatically reactivate workers whenever a new /// version is deployed. // TODO(jonahwilliams): on re-activate, only evict stale assets. String generateServiceWorker(Map<String, String> resources) { return ''' 'use strict'; const CACHE_NAME = 'flutter-app-cache'; const RESOURCES = { ${resources.entries.map((MapEntry<String, String> entry) => '"${entry.key}": "${entry.value}"').join(",\n")} }; self.addEventListener('activate', function (event) { event.waitUntil( caches.keys().then(function (cacheName) { return caches.delete(cacheName); }).then(function (_) { return caches.open(CACHE_NAME); }).then(function (cache) { return cache.addAll(Object.keys(RESOURCES)); }) ); }); self.addEventListener('fetch', function (event) { event.respondWith( caches.match(event.request) .then(function (response) { if (response) { return response; } return fetch(event.request); }) ); }); '''; }