1// Copyright 2019 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// ignore_for_file: implementation_imports 6import 'dart:async'; 7import 'dart:convert'; // ignore: dart_convert_import 8import 'dart:io'; // ignore: dart_io_import 9import 'dart:isolate'; 10 11import 'package:analyzer/dart/analysis/results.dart'; 12import 'package:analyzer/dart/analysis/utilities.dart'; 13import 'package:analyzer/dart/ast/ast.dart'; 14import 'package:build/build.dart'; 15import 'package:build_config/build_config.dart'; 16import 'package:build_modules/build_modules.dart'; 17import 'package:build_modules/builders.dart'; 18import 'package:build_modules/src/module_builder.dart'; 19import 'package:build_modules/src/platform.dart'; 20import 'package:build_modules/src/workers.dart'; 21import 'package:build_runner/build_runner.dart' as build_runner; 22import 'package:build_runner_core/build_runner_core.dart' as core; 23import 'package:build_test/builder.dart'; 24import 'package:build_test/src/debug_test_builder.dart'; 25import 'package:build_web_compilers/build_web_compilers.dart'; 26import 'package:build_web_compilers/builders.dart'; 27import 'package:build_web_compilers/src/dev_compiler_bootstrap.dart'; 28import 'package:crypto/crypto.dart'; 29import 'package:path/path.dart' as path; // ignore: package_path_import 30import 'package:scratch_space/scratch_space.dart'; 31import 'package:test_core/backend.dart'; 32 33const String ddcBootstrapExtension = '.dart.bootstrap.js'; 34const String jsEntrypointExtension = '.dart.js'; 35const String jsEntrypointSourceMapExtension = '.dart.js.map'; 36const String jsEntrypointArchiveExtension = '.dart.js.tar.gz'; 37const String digestsEntrypointExtension = '.digests'; 38const String jsModuleErrorsExtension = '.ddc.js.errors'; 39const String jsModuleExtension = '.ddc.js'; 40const String jsSourceMapExtension = '.ddc.js.map'; 41 42final DartPlatform flutterWebPlatform = 43 DartPlatform.register('flutter_web', <String>[ 44 'async', 45 'collection', 46 'convert', 47 'core', 48 'developer', 49 'html', 50 'html_common', 51 'indexed_db', 52 'js', 53 'js_util', 54 'math', 55 'svg', 56 'typed_data', 57 'web_audio', 58 'web_gl', 59 'web_sql', 60 '_internal', 61 // Flutter web specific libraries. 62 'ui', 63 '_engine', 64 'io', 65 'isolate', 66]); 67 68/// The builders required to compile a Flutter application to the web. 69final List<core.BuilderApplication> builders = <core.BuilderApplication>[ 70 core.apply( 71 'flutter_tools:test_bootstrap', 72 <BuilderFactory>[ 73 (BuilderOptions options) => const DebugTestBuilder(), 74 (BuilderOptions options) => const FlutterWebTestBootstrapBuilder(), 75 ], 76 core.toRoot(), 77 hideOutput: true, 78 defaultGenerateFor: const InputSet( 79 include: <String>[ 80 'test/**', 81 ], 82 ), 83 ), 84 core.apply( 85 'flutter_tools:shell', 86 <BuilderFactory>[ 87 (BuilderOptions options) => const FlutterWebShellBuilder(), 88 ], 89 core.toRoot(), 90 hideOutput: true, 91 defaultGenerateFor: const InputSet( 92 include: <String>[ 93 'lib/**', 94 'web/**', 95 ], 96 ), 97 ), 98 core.apply( 99 'flutter_tools:module_library', 100 <Builder Function(BuilderOptions)>[moduleLibraryBuilder], 101 core.toAllPackages(), 102 isOptional: true, 103 hideOutput: true, 104 appliesBuilders: <String>['flutter_tools:module_cleanup']), 105 core.apply( 106 'flutter_tools:ddc_modules', 107 <Builder Function(BuilderOptions)>[ 108 (BuilderOptions options) => MetaModuleBuilder(flutterWebPlatform), 109 (BuilderOptions options) => MetaModuleCleanBuilder(flutterWebPlatform), 110 (BuilderOptions options) => ModuleBuilder(flutterWebPlatform), 111 ], 112 core.toNoneByDefault(), 113 isOptional: true, 114 hideOutput: true, 115 appliesBuilders: <String>['flutter_tools:module_cleanup']), 116 core.apply( 117 'flutter_tools:ddc', 118 <Builder Function(BuilderOptions)>[ 119 (BuilderOptions builderOptions) => KernelBuilder( 120 platformSdk: builderOptions.config['flutterWebSdk'], 121 summaryOnly: true, 122 sdkKernelPath: path.join('kernel', 'flutter_ddc_sdk.dill'), 123 outputExtension: ddcKernelExtension, 124 platform: flutterWebPlatform, 125 librariesPath: 'libraries.json', 126 kernelTargetName: 'ddc', 127 ), 128 (BuilderOptions builderOptions) => DevCompilerBuilder( 129 useIncrementalCompiler: false, 130 platform: flutterWebPlatform, 131 platformSdk: builderOptions.config['flutterWebSdk'], 132 sdkKernelPath: path.url.join('kernel', 'flutter_ddc_sdk.dill'), 133 librariesPath: 'libraries.json', 134 ), 135 ], 136 core.toAllPackages(), 137 isOptional: true, 138 hideOutput: true, 139 appliesBuilders: <String>['flutter_tools:ddc_modules']), 140 core.apply( 141 'flutter_tools:entrypoint', 142 <BuilderFactory>[ 143 (BuilderOptions options) => FlutterWebEntrypointBuilder( 144 options.config['release'] ?? false, 145 options.config['flutterWebSdk'], 146 ), 147 ], 148 core.toRoot(), 149 hideOutput: true, 150 defaultGenerateFor: const InputSet( 151 include: <String>[ 152 'lib/**_web_entrypoint.dart', 153 ], 154 ), 155 ), 156 core.apply( 157 'flutter_tools:test_entrypoint', 158 <BuilderFactory>[ 159 (BuilderOptions options) => const FlutterWebTestEntrypointBuilder(), 160 ], 161 core.toRoot(), 162 hideOutput: true, 163 defaultGenerateFor: const InputSet( 164 include: <String>[ 165 'test/**_test.dart.browser_test.dart', 166 ], 167 ), 168 ), 169 core.applyPostProcess('flutter_tools:module_cleanup', moduleCleanup, 170 defaultGenerateFor: const InputSet()) 171]; 172 173/// The entrypoint to this build script. 174Future<void> main(List<String> args, [SendPort sendPort]) async { 175 core.overrideGeneratedOutputDirectory('flutter_web'); 176 final int result = await build_runner.run(args, builders); 177 sendPort?.send(result); 178} 179 180/// A ddc-only entrypoint builder that respects the Flutter target flag. 181class FlutterWebTestEntrypointBuilder implements Builder { 182 const FlutterWebTestEntrypointBuilder(); 183 184 @override 185 Map<String, List<String>> get buildExtensions => const <String, List<String>>{ 186 '.dart': <String>[ 187 ddcBootstrapExtension, 188 jsEntrypointExtension, 189 jsEntrypointSourceMapExtension, 190 jsEntrypointArchiveExtension, 191 digestsEntrypointExtension, 192 ], 193 }; 194 195 @override 196 Future<void> build(BuildStep buildStep) async { 197 log.info('building for target ${buildStep.inputId.path}'); 198 await bootstrapDdc(buildStep, platform: flutterWebPlatform); 199 } 200} 201 202/// A ddc-only entrypoint builder that respects the Flutter target flag. 203class FlutterWebEntrypointBuilder implements Builder { 204 const FlutterWebEntrypointBuilder(this.release, this.flutterWebSdk); 205 206 final bool release; 207 final String flutterWebSdk; 208 209 @override 210 Map<String, List<String>> get buildExtensions => const <String, List<String>>{ 211 '.dart': <String>[ 212 ddcBootstrapExtension, 213 jsEntrypointExtension, 214 jsEntrypointSourceMapExtension, 215 jsEntrypointArchiveExtension, 216 digestsEntrypointExtension, 217 ], 218 }; 219 220 @override 221 Future<void> build(BuildStep buildStep) async { 222 if (release) { 223 await bootstrapDart2Js(buildStep, flutterWebSdk); 224 } else { 225 await bootstrapDdc(buildStep, platform: flutterWebPlatform); 226 } 227 } 228} 229 230/// Bootstraps the test entrypoint. 231class FlutterWebTestBootstrapBuilder implements Builder { 232 const FlutterWebTestBootstrapBuilder(); 233 234 @override 235 Map<String, List<String>> get buildExtensions => const <String, List<String>>{ 236 '_test.dart': <String>[ 237 '_test.dart.browser_test.dart', 238 ] 239 }; 240 241 @override 242 Future<void> build(BuildStep buildStep) async { 243 final AssetId id = buildStep.inputId; 244 final String contents = await buildStep.readAsString(id); 245 final String assetPath = id.pathSegments.first == 'lib' 246 ? path.url.join('packages', id.package, id.path) 247 : id.path; 248 final Metadata metadata = parseMetadata( 249 assetPath, contents, Runtime.builtIn.map((Runtime runtime) => runtime.name).toSet()); 250 251 if (metadata.testOn.evaluate(SuitePlatform(Runtime.chrome))) { 252 await buildStep.writeAsString(id.addExtension('.browser_test.dart'), ''' 253import 'dart:ui' as ui; 254import 'dart:html'; 255import 'dart:js'; 256 257import 'package:stream_channel/stream_channel.dart'; 258import 'package:test_api/src/backend/stack_trace_formatter.dart'; // ignore: implementation_imports 259import 'package:test_api/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports 260import 'package:test_api/src/remote_listener.dart'; // ignore: implementation_imports 261import 'package:test_api/src/suite_channel_manager.dart'; // ignore: implementation_imports 262 263import "${path.url.basename(id.path)}" as test; 264 265Future<void> main() async { 266 // Extra initialization for flutter_web. 267 // The following parameters are hard-coded in Flutter's test embedder. Since 268 // we don't have an embedder yet this is the lowest-most layer we can put 269 // this stuff in. 270 await ui.webOnlyInitializeEngine(); 271 // TODO(flutterweb): remove need for dynamic cast. 272 (ui.window as dynamic).debugOverrideDevicePixelRatio(3.0); 273 (ui.window as dynamic).webOnlyDebugPhysicalSizeOverride = const ui.Size(2400, 1800); 274 internalBootstrapBrowserTest(() => test.main); 275} 276 277void internalBootstrapBrowserTest(Function getMain()) { 278 var channel = 279 serializeSuite(getMain, hidePrints: false, beforeLoad: () async { 280 var serialized = 281 await suiteChannel("test.browser.mapper").stream.first as Map; 282 if (serialized == null) return; 283 }); 284 postMessageChannel().pipe(channel); 285} 286StreamChannel serializeSuite(Function getMain(), 287 {bool hidePrints = true, Future beforeLoad()}) => 288 RemoteListener.start(getMain, 289 hidePrints: hidePrints, beforeLoad: beforeLoad); 290 291StreamChannel suiteChannel(String name) { 292 var manager = SuiteChannelManager.current; 293 if (manager == null) { 294 throw StateError('suiteChannel() may only be called within a test worker.'); 295 } 296 297 return manager.connectOut(name); 298} 299 300StreamChannel postMessageChannel() { 301 var controller = StreamChannelController(sync: true); 302 window.onMessage.firstWhere((message) { 303 return message.origin == window.location.origin && message.data == "port"; 304 }).then((message) { 305 var port = message.ports.first; 306 var portSubscription = port.onMessage.listen((message) { 307 controller.local.sink.add(message.data); 308 }); 309 310 controller.local.stream.listen((data) { 311 port.postMessage({"data": data}); 312 }, onDone: () { 313 port.postMessage({"event": "done"}); 314 portSubscription.cancel(); 315 }); 316 }); 317 318 context['parent'].callMethod('postMessage', [ 319 JsObject.jsify({"href": window.location.href, "ready": true}), 320 window.location.origin, 321 ]); 322 return controller.foreign; 323} 324 325void setStackTraceMapper(StackTraceMapper mapper) { 326 var formatter = StackTraceFormatter.current; 327 if (formatter == null) { 328 throw StateError( 329 'setStackTraceMapper() may only be called within a test worker.'); 330 } 331 332 formatter.configure(mapper: mapper); 333} 334'''); 335 } 336 } 337} 338 339/// A shell builder which generates the web specific entrypoint. 340class FlutterWebShellBuilder implements Builder { 341 const FlutterWebShellBuilder(); 342 343 @override 344 Future<void> build(BuildStep buildStep) async { 345 final AssetId dartEntrypointId = buildStep.inputId; 346 final bool isAppEntrypoint = await _isAppEntryPoint(dartEntrypointId, buildStep); 347 if (!isAppEntrypoint) { 348 return; 349 } 350 final AssetId outputId = buildStep.inputId.changeExtension('_web_entrypoint.dart'); 351 await buildStep.writeAsString(outputId, ''' 352import 'dart:ui' as ui; 353import "${path.url.basename(buildStep.inputId.path)}" as entrypoint; 354 355Future<void> main() async { 356 await ui.webOnlyInitializePlatform(); 357 entrypoint.main(); 358} 359 360'''); 361 } 362 363 @override 364 Map<String, List<String>> get buildExtensions => const <String, List<String>>{ 365 '.dart': <String>['_web_entrypoint.dart'], 366 }; 367} 368 369Future<void> bootstrapDart2Js(BuildStep buildStep, String flutterWebSdk) async { 370 final AssetId dartEntrypointId = buildStep.inputId; 371 final AssetId moduleId = dartEntrypointId.changeExtension(moduleExtension(flutterWebPlatform)); 372 final Module module = Module.fromJson(json.decode(await buildStep.readAsString(moduleId))); 373 374 final List<Module> allDeps = await module.computeTransitiveDependencies(buildStep, throwIfUnsupported: false)..add(module); 375 final ScratchSpace scratchSpace = await buildStep.fetchResource(scratchSpaceResource); 376 final Iterable<AssetId> allSrcs = allDeps.expand((Module module) => module.sources); 377 await scratchSpace.ensureAssets(allSrcs, buildStep); 378 379 final String packageFile = await _createPackageFile(allSrcs, buildStep, scratchSpace); 380 final String dartPath = dartEntrypointId.path.startsWith('lib/') 381 ? 'package:${dartEntrypointId.package}/' 382 '${dartEntrypointId.path.substring('lib/'.length)}' 383 : dartEntrypointId.path; 384 final String jsOutputPath = 385 '${path.withoutExtension(dartPath.replaceFirst('package:', 'packages/'))}' 386 '$jsEntrypointExtension'; 387 final String flutterWebSdkPath = flutterWebSdk; 388 final String librariesPath = path.join(flutterWebSdkPath, 'libraries.json'); 389 final List<String> args = <String>[ 390 '--libraries-spec="$librariesPath"', 391 '-O4', 392 '-o', 393 '$jsOutputPath', 394 '--packages="$packageFile"', 395 '-Ddart.vm.product=true', 396 dartPath, 397 ]; 398 final Dart2JsBatchWorkerPool dart2js = await buildStep.fetchResource(dart2JsWorkerResource); 399 final Dart2JsResult result = await dart2js.compile(args); 400 final AssetId jsOutputId = dartEntrypointId.changeExtension(jsEntrypointExtension); 401 final File jsOutputFile = scratchSpace.fileFor(jsOutputId); 402 if (result.succeeded && jsOutputFile.existsSync()) { 403 log.info(result.output); 404 // Explicitly write out the original js file and sourcemap. 405 await scratchSpace.copyOutput(jsOutputId, buildStep); 406 final AssetId jsSourceMapId = 407 dartEntrypointId.changeExtension(jsEntrypointSourceMapExtension); 408 await _copyIfExists(jsSourceMapId, scratchSpace, buildStep); 409 } else { 410 log.severe(result.output); 411 } 412} 413 414Future<void> _copyIfExists( 415 AssetId id, ScratchSpace scratchSpace, AssetWriter writer) async { 416 final File file = scratchSpace.fileFor(id); 417 if (file.existsSync()) { 418 await scratchSpace.copyOutput(id, writer); 419 } 420} 421 422/// Creates a `.packages` file unique to this entrypoint at the root of the 423/// scratch space and returns it's filename. 424/// 425/// Since mulitple invocations of Dart2Js will share a scratch space and we only 426/// know the set of packages involved the current entrypoint we can't construct 427/// a `.packages` file that will work for all invocations of Dart2Js so a unique 428/// file is created for every entrypoint that is run. 429/// 430/// The filename is based off the MD5 hash of the asset path so that files are 431/// unique regarless of situations like `web/foo/bar.dart` vs 432/// `web/foo-bar.dart`. 433Future<String> _createPackageFile(Iterable<AssetId> inputSources, BuildStep buildStep, ScratchSpace scratchSpace) async { 434 final Uri inputUri = buildStep.inputId.uri; 435 final String packageFileName = 436 '.package-${md5.convert(inputUri.toString().codeUnits)}'; 437 final File packagesFile = 438 scratchSpace.fileFor(AssetId(buildStep.inputId.package, packageFileName)); 439 final Set<String> packageNames = inputSources.map((AssetId s) => s.package).toSet(); 440 final String packagesFileContent = 441 packageNames.map((String name) => '$name:packages/$name/').join('\n'); 442 await packagesFile 443 .writeAsString('# Generated for $inputUri\n$packagesFileContent'); 444 return packageFileName; 445} 446 447/// Returns whether or not [dartId] is an app entrypoint (basically, whether 448/// or not it has a `main` function). 449Future<bool> _isAppEntryPoint(AssetId dartId, AssetReader reader) async { 450 assert(dartId.extension == '.dart'); 451 // Skip reporting errors here, dartdevc will report them later with nicer 452 // formatting. 453 final ParseStringResult result = parseString( 454 content: await reader.readAsString(dartId), 455 throwIfDiagnostics: false, 456 ); 457 // Allow two or fewer arguments so that entrypoints intended for use with 458 // [spawnUri] get counted. 459 return result.unit.declarations.any((CompilationUnitMember node) { 460 return node is FunctionDeclaration && 461 node.name.name == 'main' && 462 node.functionExpression.parameters.parameters.length <= 2; 463 }); 464} 465