• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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