Version 2.16.0-60.0.dev

Merge commit 'b735151bbccdcbe216f45eb511ab01d1650b0eb2' into 'dev'
diff --git a/pkg/analyzer/lib/src/services/available_declarations.dart b/pkg/analyzer/lib/src/services/available_declarations.dart
index 588b150..b5a3097 100644
--- a/pkg/analyzer/lib/src/services/available_declarations.dart
+++ b/pkg/analyzer/lib/src/services/available_declarations.dart
@@ -504,6 +504,10 @@
   /// The list of changed file paths.
   final List<String> _changedPaths = [];
 
+  /// While processing [_changedPaths] we accumulate libraries here.
+  /// Then we process all of them at once, and reset to the empty set.
+  Set<_File> _invalidatedLibraries = {};
+
   /// The list of files scheduled for processing.  It may include parts and
   /// libraries, but parts are ignored when we detect them.
   final List<_ScheduledFile> _scheduledFiles = [];
@@ -529,7 +533,9 @@
       _whenKnownFilesPulled = now;
       pullKnownFiles();
     }
-    return _changedPaths.isNotEmpty || _scheduledFiles.isNotEmpty;
+    return _changedPaths.isNotEmpty ||
+        _invalidatedLibraries.isNotEmpty ||
+        _scheduledFiles.isNotEmpty;
   }
 
   /// Add the [analysisContext], so that its libraries are reported via the
@@ -595,6 +601,13 @@
       return;
     }
 
+    // There are no more changes as far as we know.
+    // So, recompute exported declarations for all invalidated libraries.
+    if (_invalidatedLibraries.isNotEmpty) {
+      _processInvalidatedLibraries(_invalidatedLibraries);
+      _invalidatedLibraries = {};
+    }
+
     if (_scheduledFiles.isNotEmpty) {
       var scheduledFile = _scheduledFiles.removeLast();
       var file = _getFileByPath(scheduledFile.context, [], scheduledFile.path)!;
@@ -794,11 +807,33 @@
         _invalidateExportedDeclarations(invalidatedLibraries, newLibrary);
       }
     }
-    _computeExportedDeclarations(invalidatedLibraries);
 
-    var changedLibraries = <Library>[];
+    // Don't compute exported declarations now, there might be more changes.
+    // Instead, accumulate invalidated libraries, and recompute all later.
+    _invalidatedLibraries.addAll(invalidatedLibraries);
+
     var removedLibraries = <int>[];
     for (var libraryFile in invalidatedLibraries) {
+      if (!libraryFile.exists) {
+        _idToLibrary.remove(libraryFile.id);
+        removedLibraries.add(libraryFile.id);
+      }
+    }
+    for (var file in notLibraries) {
+      _idToLibrary.remove(file.id);
+      removedLibraries.add(file.id);
+    }
+    if (removedLibraries.isNotEmpty) {
+      _changesController.add(
+        LibraryChange._([], removedLibraries),
+      );
+    }
+  }
+
+  void _processInvalidatedLibraries(Set<_File> invalidatedLibraries) {
+    _computeExportedDeclarations(invalidatedLibraries);
+    var changedLibraries = <Library>[];
+    for (var libraryFile in invalidatedLibraries) {
       if (libraryFile.exists) {
         var library = Library._(
           libraryFile.id,
@@ -809,17 +844,10 @@
         );
         _idToLibrary[library.id] = library;
         changedLibraries.add(library);
-      } else {
-        _idToLibrary.remove(libraryFile.id);
-        removedLibraries.add(libraryFile.id);
       }
     }
-    for (var file in notLibraries) {
-      _idToLibrary.remove(file.id);
-      removedLibraries.add(file.id);
-    }
     _changesController.add(
-      LibraryChange._(changedLibraries, removedLibraries),
+      LibraryChange._(changedLibraries, []),
     );
   }
 
diff --git a/pkg/analyzer/test/src/services/available_declarations_test.dart b/pkg/analyzer/test/src/services/available_declarations_test.dart
index 17908dd..357ca89 100644
--- a/pkg/analyzer/test/src/services/available_declarations_test.dart
+++ b/pkg/analyzer/test/src/services/available_declarations_test.dart
@@ -914,6 +914,75 @@
     ]);
   }
 
+  /// https://github.com/dart-lang/sdk/issues/47804
+  test_updated_exported2() async {
+    var a = convertPath('/home/test/lib/a.dart');
+    var b = convertPath('/home/test/lib/b.dart');
+    var c = convertPath('/home/test/lib/c.dart');
+
+    newFile(a, content: r'''
+class A {}
+''');
+    newFile(b, content: r'''
+class B {}
+''');
+    newFile(c, content: r'''
+export 'a.dart';
+export 'b.dart';
+class C {}
+''');
+    tracker.addContext(testAnalysisContext);
+
+    await _doAllTrackerWork();
+    _assertHasLibrary('package:test/c.dart', declarations: [
+      _ExpectedDeclaration.class_('A', [
+        _ExpectedDeclaration.constructor(''),
+      ]),
+      _ExpectedDeclaration.class_('B', [
+        _ExpectedDeclaration.constructor(''),
+      ]),
+      _ExpectedDeclaration.class_('C', [
+        _ExpectedDeclaration.constructor(''),
+      ]),
+    ]);
+
+    changes.clear();
+
+    newFile(a, content: r'''
+class A2 {}
+''');
+    newFile(b, content: r'''
+class B2 {}
+''');
+    tracker.changeFile(a);
+    tracker.changeFile(b);
+    await _doAllTrackerWork();
+
+    // In general it is OK to get duplicate libraries.
+    // But here we notified about both `a.dart` and `b.dart` changes before
+    // performing any work. So, there is no reason do handle `c.dart` twice.
+    var uniquePathSet = <String>{};
+    for (var change in changes) {
+      for (var library in change.changed) {
+        if (!uniquePathSet.add(library.path)) {
+          fail('Not unique path: ${library.path}');
+        }
+      }
+    }
+
+    _assertHasLibrary('package:test/c.dart', declarations: [
+      _ExpectedDeclaration.class_('A2', [
+        _ExpectedDeclaration.constructor(''),
+      ]),
+      _ExpectedDeclaration.class_('B2', [
+        _ExpectedDeclaration.constructor(''),
+      ]),
+      _ExpectedDeclaration.class_('C', [
+        _ExpectedDeclaration.constructor(''),
+      ]),
+    ]);
+  }
+
   test_updated_library() async {
     var a = convertPath('/home/test/lib/a.dart');
     var b = convertPath('/home/test/lib/b.dart');
diff --git a/pkg/compiler/tool/modular_test_suite.dart b/pkg/compiler/tool/modular_test_suite.dart
index 9257730..7a20a67 100644
--- a/pkg/compiler/tool/modular_test_suite.dart
+++ b/pkg/compiler/tool/modular_test_suite.dart
@@ -5,44 +5,25 @@
 /// Test the modular compilation pipeline of dart2js.
 ///
 /// This is a shell that runs multiple tests, one per folder under `data/`.
-import 'dart:io';
 import 'dart:async';
 
-import 'package:compiler/src/commandline_options.dart';
-import 'package:compiler/src/kernel/dart2js_target.dart';
-import 'package:front_end/src/compute_platform_binaries_location.dart'
-    show computePlatformBinariesLocation;
 import 'package:modular_test/src/io_pipeline.dart';
-import 'package:modular_test/src/pipeline.dart';
-import 'package:modular_test/src/suite.dart';
 import 'package:modular_test/src/runner.dart';
-import 'package:package_config/package_config.dart';
+import 'modular_test_suite_helper.dart';
 
-String packageConfigJsonPath = ".dart_tool/package_config.json";
-Uri sdkRoot = Platform.script.resolve("../../../");
-Uri packageConfigUri = sdkRoot.resolve(packageConfigJsonPath);
-Options _options;
-String _dart2jsScript;
-String _kernelWorkerScript;
-
-// TODO(joshualitt): Figure out a way to support package configs in
-// tests/modular.
-PackageConfig _packageConfig;
 main(List<String> args) async {
-  _options = Options.parse(args);
-  _packageConfig = await loadPackageConfigUri(packageConfigUri);
-  await _resolveScripts();
+  var options = Options.parse(args);
+  await resolveScripts(options);
   await Future.wait([
-    // TODO(joshualitt): Factor modular steps into a library so we can test
-    // modular analysis alongside the existing pipeline.
     runSuite(
         sdkRoot.resolve('tests/modular/'),
         'tests/modular',
-        _options,
+        options,
         IOPipeline([
           OutlineDillCompilationStep(),
           FullDillCompilationStep(),
-          ComputeClosedWorldStep(useModularAnalysis: false),
+          ModularAnalysisStep(),
+          ComputeClosedWorldStep(useModularAnalysis: true),
           GlobalAnalysisStep(),
           Dart2jsCodegenStep(codeId0),
           Dart2jsCodegenStep(codeId1),
@@ -51,631 +32,3 @@
         ], cacheSharedModules: true)),
   ]);
 }
-
-const dillSummaryId = DataId("sdill");
-const dillId = DataId("dill");
-const modularUpdatedDillId = DataId("mdill");
-const modularDataId = DataId("mdata");
-const closedWorldId = DataId("world");
-const globalUpdatedDillId = DataId("gdill");
-const globalDataId = DataId("gdata");
-const codeId = ShardsDataId("code", 2);
-const codeId0 = ShardDataId(codeId, 0);
-const codeId1 = ShardDataId(codeId, 1);
-const jsId = DataId("js");
-const txtId = DataId("txt");
-const fakeRoot = 'dev-dart-app:/';
-
-String _packageConfigEntry(String name, Uri root,
-    {Uri packageRoot, LanguageVersion version}) {
-  var fields = [
-    '"name": "${name}"',
-    '"rootUri": "$root"',
-    if (packageRoot != null) '"packageUri": "$packageRoot"',
-    if (version != null) '"languageVersion": "$version"'
-  ];
-  return '{${fields.join(',')}}';
-}
-
-abstract class CFEStep implements IOModularStep {
-  final String stepName;
-
-  CFEStep(this.stepName);
-
-  @override
-  bool get needsSources => true;
-
-  @override
-  bool get onlyOnMain => false;
-
-  @override
-  Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
-      List<String> flags) async {
-    if (_options.verbose) print("\nstep: $stepName on $module");
-
-    // We use non file-URI schemes for representeing source locations in a
-    // root-agnostic way. This allows us to refer to file across modules and
-    // across steps without exposing the underlying temporary folders that are
-    // created by the framework. In build systems like bazel this is especially
-    // important because each step may be run on a different machine.
-    //
-    // Files in packages are defined in terms of `package:` URIs, while
-    // non-package URIs are defined using the `dart-dev-app` scheme.
-    String rootScheme = module.isSdk ? 'dart-dev-sdk' : 'dev-dart-app';
-    String sourceToImportUri(Uri relativeUri) {
-      if (module.isPackage) {
-        var basePath = module.packageBase.path;
-        var packageRelativePath = basePath == "./"
-            ? relativeUri.path
-            : relativeUri.path.substring(basePath.length);
-        return 'package:${module.name}/$packageRelativePath';
-      } else {
-        return '$rootScheme:/$relativeUri';
-      }
-    }
-
-    // We create both a .packages and package_config.json file which defines
-    // the location of this module if it is a package.  The CFE requires that
-    // if a `package:` URI of a dependency is used in an import, then we need
-    // that package entry in the associated file. However, after it checks that
-    // the definition exists, the CFE will not actually use the resolved URI if
-    // a library for the import URI is already found in one of the provide
-    // .dill files of the dependencies. For that reason, and to ensure that
-    // a step only has access to the files provided in a module, we generate a
-    // config file with invalid folders for other packages.
-    // TODO(sigmund): follow up with the CFE to see if we can remove the need
-    // for these dummy entries..
-    // TODO(joshualitt): Generate just the json file.
-    var packagesJson = [];
-    var packagesContents = StringBuffer();
-    if (module.isPackage) {
-      packagesContents.write('${module.name}:${module.packageBase}\n');
-      packagesJson.add(_packageConfigEntry(
-          module.name, Uri.parse('../${module.packageBase}')));
-    }
-
-    Set<Module> transitiveDependencies = computeTransitiveDependencies(module);
-    int unusedNum = 0;
-    for (Module dependency in transitiveDependencies) {
-      if (dependency.isPackage) {
-        // rootUri should be ignored for dependent modules, so we pass in a
-        // bogus value.
-        var rootUri = Uri.parse('unused$unusedNum');
-        unusedNum++;
-
-        var dependentPackage = _packageConfig[dependency.name];
-        var packageJson = dependentPackage == null
-            ? _packageConfigEntry(dependency.name, rootUri)
-            : _packageConfigEntry(dependentPackage.name, rootUri,
-                version: dependentPackage.languageVersion);
-        packagesJson.add(packageJson);
-        packagesContents.write('${dependency.name}:$rootUri\n');
-      }
-    }
-
-    if (module.isPackage) {
-      await File.fromUri(root.resolve(packageConfigJsonPath))
-          .create(recursive: true);
-      await File.fromUri(root.resolve(packageConfigJsonPath)).writeAsString('{'
-          '  "configVersion": ${_packageConfig.version},'
-          '  "packages": [ ${packagesJson.join(',')} ]'
-          '}');
-    }
-
-    await File.fromUri(root.resolve('.packages'))
-        .writeAsString('$packagesContents');
-
-    List<String> sources;
-    List<String> extraArgs = ['--packages-file', '$rootScheme:/.packages'];
-    if (module.isSdk) {
-      // When no flags are passed, we can skip compilation and reuse the
-      // platform.dill created by build.py.
-      if (flags.isEmpty) {
-        var platform = computePlatformBinariesLocation()
-            .resolve("dart2js_platform_unsound.dill");
-        var destination = root.resolveUri(toUri(module, outputData));
-        if (_options.verbose) {
-          print('command:\ncp $platform $destination');
-        }
-        await File.fromUri(platform).copy(destination.toFilePath());
-        return;
-      }
-      sources = requiredLibraries['dart2js'] + ['dart:core'];
-      extraArgs += [
-        '--libraries-file',
-        '$rootScheme:///sdk/lib/libraries.json'
-      ];
-      assert(transitiveDependencies.isEmpty);
-    } else {
-      sources = module.sources.map(sourceToImportUri).toList();
-    }
-
-    // TODO(joshualitt): Ensure the kernel worker has some way to specify
-    // --no-sound-null-safety
-    List<String> args = [
-      _kernelWorkerScript,
-      ...stepArguments,
-      '--exclude-non-sources',
-      '--multi-root',
-      '$root',
-      '--multi-root-scheme',
-      rootScheme,
-      ...extraArgs,
-      '--output',
-      '${toUri(module, outputData)}',
-      ...(transitiveDependencies
-          .expand((m) => ['--input-summary', '${toUri(m, inputData)}'])),
-      ...(sources.expand((String uri) => ['--source', uri])),
-      ...(flags.expand((String flag) => ['--enable-experiment', flag])),
-    ];
-
-    var result =
-        await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
-    _checkExitCode(result, this, module);
-  }
-
-  List<String> get stepArguments;
-
-  DataId get inputData;
-
-  DataId get outputData;
-
-  @override
-  void notifyCached(Module module) {
-    if (_options.verbose) print("\ncached step: $stepName on $module");
-  }
-}
-
-// Step that compiles sources in a module to a summary .dill file.
-class OutlineDillCompilationStep extends CFEStep {
-  @override
-  List<DataId> get resultData => const [dillSummaryId];
-
-  @override
-  bool get needsSources => true;
-
-  @override
-  List<DataId> get dependencyDataNeeded => const [dillSummaryId];
-
-  @override
-  List<DataId> get moduleDataNeeded => const [];
-
-  @override
-  List<String> get stepArguments =>
-      ['--target', 'dart2js_summary', '--summary-only'];
-
-  @override
-  DataId get inputData => dillSummaryId;
-
-  @override
-  DataId get outputData => dillSummaryId;
-
-  OutlineDillCompilationStep() : super('outline-dill-compilation');
-}
-
-// Step that compiles sources in a module to a .dill file.
-class FullDillCompilationStep extends CFEStep {
-  @override
-  List<DataId> get resultData => const [dillId];
-
-  @override
-  bool get needsSources => true;
-
-  @override
-  List<DataId> get dependencyDataNeeded => const [dillSummaryId];
-
-  @override
-  List<DataId> get moduleDataNeeded => const [];
-
-  @override
-  List<String> get stepArguments =>
-      ['--target', 'dart2js', '--no-summary', '--no-summary-only'];
-
-  @override
-  DataId get inputData => dillSummaryId;
-
-  @override
-  DataId get outputData => dillId;
-
-  FullDillCompilationStep() : super('full-dill-compilation');
-}
-
-class ModularAnalysisStep implements IOModularStep {
-  @override
-  List<DataId> get resultData => const [modularDataId, modularUpdatedDillId];
-
-  @override
-  bool get needsSources => false;
-
-  @override
-  List<DataId> get dependencyDataNeeded => const [dillId];
-
-  @override
-  List<DataId> get moduleDataNeeded => const [dillId];
-
-  @override
-  bool get onlyOnMain => false;
-
-  @override
-  Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
-      List<String> flags) async {
-    if (_options.verbose) print("\nstep: modular analysis on $module");
-    Set<Module> transitiveDependencies = computeTransitiveDependencies(module);
-    Iterable<String> dillDependencies =
-        transitiveDependencies.map((m) => '${toUri(m, dillId)}');
-    List<String> args = [
-      '--packages=${sdkRoot.toFilePath()}/.packages',
-      _dart2jsScript,
-      if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
-      '${Flags.inputDill}=${toUri(module, dillId)}',
-      if (dillDependencies.isNotEmpty)
-        '--dill-dependencies=${dillDependencies.join(',')}',
-      '--out=${toUri(module, modularUpdatedDillId)}',
-      '${Flags.writeModularAnalysis}=${toUri(module, modularDataId)}',
-      for (String flag in flags) '--enable-experiment=$flag',
-    ];
-    var result =
-        await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
-
-    _checkExitCode(result, this, module);
-  }
-
-  @override
-  void notifyCached(Module module) {
-    if (_options.verbose) {
-      print("cached step: dart2js modular analysis on $module");
-    }
-  }
-}
-
-DataId idForDill({bool useModularAnalysis}) =>
-    useModularAnalysis ? modularUpdatedDillId : dillId;
-
-List<DataId> inputFromAnalysis({bool useModularAnalysis = false}) => [
-      idForDill(useModularAnalysis: useModularAnalysis),
-      if (useModularAnalysis) modularDataId,
-    ];
-
-// Step that invokes the dart2js closed world computation.
-class ComputeClosedWorldStep implements IOModularStep {
-  final bool useModularAnalysis;
-
-  ComputeClosedWorldStep({this.useModularAnalysis});
-
-  @override
-  List<DataId> get resultData => const [closedWorldId, globalUpdatedDillId];
-
-  @override
-  bool get needsSources => false;
-
-  @override
-  List<DataId> get dependencyDataNeeded =>
-      inputFromAnalysis(useModularAnalysis: useModularAnalysis);
-
-  @override
-  List<DataId> get moduleDataNeeded =>
-      inputFromAnalysis(useModularAnalysis: useModularAnalysis);
-
-  @override
-  bool get onlyOnMain => true;
-
-  @override
-  Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
-      List<String> flags) async {
-    if (_options.verbose)
-      print("\nstep: dart2js compute closed world on $module");
-    Set<Module> transitiveDependencies = computeTransitiveDependencies(module);
-    DataId dillId = idForDill(useModularAnalysis: useModularAnalysis);
-    Iterable<String> dillDependencies =
-        transitiveDependencies.map((m) => '${toUri(m, dillId)}');
-    List<String> dataDependencies = transitiveDependencies
-        .map((m) => '${toUri(m, modularDataId)}')
-        .toList();
-    dataDependencies.add('${toUri(module, modularDataId)}');
-    List<String> args = [
-      '--packages=${sdkRoot.toFilePath()}/.packages',
-      _dart2jsScript,
-      // TODO(sigmund): remove this dependency on libraries.json
-      if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
-      '${Flags.entryUri}=$fakeRoot${module.mainSource}',
-      '${Flags.inputDill}=${toUri(module, dillId)}',
-      for (String flag in flags) '--enable-experiment=$flag',
-      '${Flags.dillDependencies}=${dillDependencies.join(',')}',
-      if (useModularAnalysis)
-        '${Flags.readModularAnalysis}=${dataDependencies.join(',')}',
-      '${Flags.writeClosedWorld}=${toUri(module, closedWorldId)}',
-      '--out=${toUri(module, globalUpdatedDillId)}',
-    ];
-    var result =
-        await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
-
-    _checkExitCode(result, this, module);
-  }
-
-  @override
-  void notifyCached(Module module) {
-    if (_options.verbose)
-      print("\ncached step: dart2js compute closed world on $module");
-  }
-}
-
-// Step that runs the dart2js modular analysis.
-class GlobalAnalysisStep implements IOModularStep {
-  @override
-  List<DataId> get resultData => const [globalDataId];
-
-  @override
-  bool get needsSources => false;
-
-  @override
-  List<DataId> get dependencyDataNeeded => const [globalUpdatedDillId];
-
-  @override
-  List<DataId> get moduleDataNeeded =>
-      const [closedWorldId, globalUpdatedDillId];
-
-  @override
-  bool get onlyOnMain => true;
-
-  @override
-  Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
-      List<String> flags) async {
-    if (_options.verbose) print("\nstep: dart2js global analysis on $module");
-    List<String> args = [
-      '--packages=${sdkRoot.toFilePath()}/.packages',
-      _dart2jsScript,
-      // TODO(sigmund): remove this dependency on libraries.json
-      if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
-      '${Flags.entryUri}=$fakeRoot${module.mainSource}',
-      '${Flags.inputDill}=${toUri(module, globalUpdatedDillId)}',
-      for (String flag in flags) '--enable-experiment=$flag',
-      '${Flags.readClosedWorld}=${toUri(module, closedWorldId)}',
-      '${Flags.writeData}=${toUri(module, globalDataId)}',
-    ];
-    var result =
-        await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
-
-    _checkExitCode(result, this, module);
-  }
-
-  @override
-  void notifyCached(Module module) {
-    if (_options.verbose)
-      print("\ncached step: dart2js global analysis on $module");
-  }
-}
-
-// Step that invokes the dart2js code generation on the main module given the
-// results of the global analysis step and produces one shard of the codegen
-// output.
-class Dart2jsCodegenStep implements IOModularStep {
-  final ShardDataId codeId;
-
-  Dart2jsCodegenStep(this.codeId);
-
-  @override
-  List<DataId> get resultData => [codeId];
-
-  @override
-  bool get needsSources => false;
-
-  @override
-  List<DataId> get dependencyDataNeeded => const [];
-
-  @override
-  List<DataId> get moduleDataNeeded =>
-      const [globalUpdatedDillId, closedWorldId, globalDataId];
-
-  @override
-  bool get onlyOnMain => true;
-
-  @override
-  Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
-      List<String> flags) async {
-    if (_options.verbose) print("\nstep: dart2js backend on $module");
-    List<String> args = [
-      '--packages=${sdkRoot.toFilePath()}/.packages',
-      _dart2jsScript,
-      if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
-      '${Flags.entryUri}=$fakeRoot${module.mainSource}',
-      '${Flags.inputDill}=${toUri(module, globalUpdatedDillId)}',
-      for (String flag in flags) '--enable-experiment=$flag',
-      '${Flags.readClosedWorld}=${toUri(module, closedWorldId)}',
-      '${Flags.readData}=${toUri(module, globalDataId)}',
-      '${Flags.writeCodegen}=${toUri(module, codeId.dataId)}',
-      '${Flags.codegenShard}=${codeId.shard}',
-      '${Flags.codegenShards}=${codeId.dataId.shards}',
-    ];
-    var result =
-        await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
-
-    _checkExitCode(result, this, module);
-  }
-
-  @override
-  void notifyCached(Module module) {
-    if (_options.verbose) print("cached step: dart2js backend on $module");
-  }
-}
-
-// Step that invokes the dart2js codegen enqueuer and emitter on the main module
-// given the results of the global analysis step and codegen shards.
-class Dart2jsEmissionStep implements IOModularStep {
-  @override
-  List<DataId> get resultData => const [jsId];
-
-  @override
-  bool get needsSources => false;
-
-  @override
-  List<DataId> get dependencyDataNeeded => const [];
-
-  @override
-  List<DataId> get moduleDataNeeded => const [
-        globalUpdatedDillId,
-        closedWorldId,
-        globalDataId,
-        codeId0,
-        codeId1
-      ];
-
-  @override
-  bool get onlyOnMain => true;
-
-  @override
-  Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
-      List<String> flags) async {
-    if (_options.verbose) print("step: dart2js backend on $module");
-    List<String> args = [
-      '--packages=${sdkRoot.toFilePath()}/.packages',
-      _dart2jsScript,
-      if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
-      '${Flags.entryUri}=$fakeRoot${module.mainSource}',
-      '${Flags.inputDill}=${toUri(module, globalUpdatedDillId)}',
-      for (String flag in flags) '${Flags.enableLanguageExperiments}=$flag',
-      '${Flags.readClosedWorld}=${toUri(module, closedWorldId)}',
-      '${Flags.readData}=${toUri(module, globalDataId)}',
-      '${Flags.readCodegen}=${toUri(module, codeId)}',
-      '${Flags.codegenShards}=${codeId.shards}',
-      '--out=${toUri(module, jsId)}',
-    ];
-    var result =
-        await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
-
-    _checkExitCode(result, this, module);
-  }
-
-  @override
-  void notifyCached(Module module) {
-    if (_options.verbose) print("\ncached step: dart2js backend on $module");
-  }
-}
-
-/// Step that runs the output of dart2js in d8 and saves the output.
-class RunD8 implements IOModularStep {
-  @override
-  List<DataId> get resultData => const [txtId];
-
-  @override
-  bool get needsSources => false;
-
-  @override
-  List<DataId> get dependencyDataNeeded => const [];
-
-  @override
-  List<DataId> get moduleDataNeeded => const [jsId];
-
-  @override
-  bool get onlyOnMain => true;
-
-  @override
-  Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
-      List<String> flags) async {
-    if (_options.verbose) print("\nstep: d8 on $module");
-    List<String> d8Args = [
-      sdkRoot
-          .resolve('sdk/lib/_internal/js_runtime/lib/preambles/d8.js')
-          .toFilePath(),
-      root.resolveUri(toUri(module, jsId)).toFilePath(),
-    ];
-    var result = await _runProcess(
-        sdkRoot.resolve(_d8executable).toFilePath(), d8Args, root.toFilePath());
-
-    _checkExitCode(result, this, module);
-
-    await File.fromUri(root.resolveUri(toUri(module, txtId)))
-        .writeAsString(result.stdout);
-  }
-
-  @override
-  void notifyCached(Module module) {
-    if (_options.verbose) print("\ncached step: d8 on $module");
-  }
-}
-
-void _checkExitCode(ProcessResult result, IOModularStep step, Module module) {
-  if (result.exitCode != 0 || _options.verbose) {
-    stdout.write(result.stdout);
-    stderr.write(result.stderr);
-  }
-  if (result.exitCode != 0) {
-    throw "${step.runtimeType} failed on $module:\n\n"
-        "stdout:\n${result.stdout}\n\n"
-        "stderr:\n${result.stderr}";
-  }
-}
-
-Future<ProcessResult> _runProcess(
-    String command, List<String> arguments, String workingDirectory) {
-  if (_options.verbose) {
-    print('command:\n$command ${arguments.join(' ')} from $workingDirectory');
-  }
-  return Process.run(command, arguments, workingDirectory: workingDirectory);
-}
-
-String get _d8executable {
-  if (Platform.isWindows) {
-    return 'third_party/d8/windows/d8.exe';
-  } else if (Platform.isLinux) {
-    return 'third_party/d8/linux/d8';
-  } else if (Platform.isMacOS) {
-    return 'third_party/d8/macos/d8';
-  }
-  throw UnsupportedError('Unsupported platform.');
-}
-
-class ShardsDataId implements DataId {
-  @override
-  final String name;
-  final int shards;
-
-  const ShardsDataId(this.name, this.shards);
-
-  @override
-  String toString() => name;
-}
-
-class ShardDataId implements DataId {
-  final ShardsDataId dataId;
-  final int _shard;
-
-  const ShardDataId(this.dataId, this._shard);
-
-  int get shard {
-    assert(0 <= _shard && _shard < dataId.shards);
-    return _shard;
-  }
-
-  @override
-  String get name => '${dataId.name}${shard}';
-
-  @override
-  String toString() => name;
-}
-
-Future<void> _resolveScripts() async {
-  Future<String> resolve(
-      String sourceUriOrPath, String relativeSnapshotPath) async {
-    Uri sourceUri = sdkRoot.resolve(sourceUriOrPath);
-    String result =
-        sourceUri.scheme == 'file' ? sourceUri.toFilePath() : sourceUriOrPath;
-    if (_options.useSdk) {
-      String snapshot = Uri.file(Platform.resolvedExecutable)
-          .resolve(relativeSnapshotPath)
-          .toFilePath();
-      if (await File(snapshot).exists()) {
-        return snapshot;
-      }
-    }
-    return result;
-  }
-
-  _dart2jsScript = await resolve(
-      'package:compiler/src/dart2js.dart', 'snapshots/dart2js.dart.snapshot');
-  _kernelWorkerScript = await resolve('utils/bazel/kernel_worker.dart',
-      'snapshots/kernel_worker.dart.snapshot');
-}
-
-String _librarySpecForSnapshot = Uri.file(Platform.resolvedExecutable)
-    .resolve('../lib/libraries.json')
-    .toFilePath();
diff --git a/pkg/compiler/tool/modular_test_suite_helper.dart b/pkg/compiler/tool/modular_test_suite_helper.dart
new file mode 100644
index 0000000..cd583ad
--- /dev/null
+++ b/pkg/compiler/tool/modular_test_suite_helper.dart
@@ -0,0 +1,662 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Test the modular compilation pipeline of dart2js.
+///
+/// This is a shell that runs multiple tests, one per folder under `data/`.
+import 'dart:io';
+import 'dart:async';
+
+import 'package:compiler/src/commandline_options.dart';
+import 'package:compiler/src/kernel/dart2js_target.dart';
+import 'package:front_end/src/compute_platform_binaries_location.dart'
+    show computePlatformBinariesLocation;
+import 'package:modular_test/src/io_pipeline.dart';
+import 'package:modular_test/src/pipeline.dart';
+import 'package:modular_test/src/suite.dart';
+import 'package:modular_test/src/runner.dart';
+import 'package:package_config/package_config.dart';
+
+String packageConfigJsonPath = ".dart_tool/package_config.json";
+Uri sdkRoot = Platform.script.resolve("../../../");
+Uri packageConfigUri = sdkRoot.resolve(packageConfigJsonPath);
+Options _options;
+String _dart2jsScript;
+String _kernelWorkerScript;
+
+const dillSummaryId = DataId("sdill");
+const dillId = DataId("dill");
+const modularUpdatedDillId = DataId("mdill");
+const modularDataId = DataId("mdata");
+const closedWorldId = DataId("world");
+const globalUpdatedDillId = DataId("gdill");
+const globalDataId = DataId("gdata");
+const codeId = ShardsDataId("code", 2);
+const codeId0 = ShardDataId(codeId, 0);
+const codeId1 = ShardDataId(codeId, 1);
+const jsId = DataId("js");
+const txtId = DataId("txt");
+const fakeRoot = 'dev-dart-app:/';
+
+String _packageConfigEntry(String name, Uri root,
+    {Uri packageRoot, LanguageVersion version}) {
+  var fields = [
+    '"name": "${name}"',
+    '"rootUri": "$root"',
+    if (packageRoot != null) '"packageUri": "$packageRoot"',
+    if (version != null) '"languageVersion": "$version"'
+  ];
+  return '{${fields.join(',')}}';
+}
+
+abstract class CFEStep implements IOModularStep {
+  final String stepName;
+
+  CFEStep(this.stepName);
+
+  @override
+  bool get needsSources => true;
+
+  @override
+  bool get onlyOnMain => false;
+
+  @override
+  Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
+      List<String> flags) async {
+    if (_options.verbose) print("\nstep: $stepName on $module");
+
+    // TODO(joshualitt): Figure out a way to support package configs in
+    // tests/modular.
+    var packageConfig = await loadPackageConfigUri(packageConfigUri);
+
+    // We use non file-URI schemes for representeing source locations in a
+    // root-agnostic way. This allows us to refer to file across modules and
+    // across steps without exposing the underlying temporary folders that are
+    // created by the framework. In build systems like bazel this is especially
+    // important because each step may be run on a different machine.
+    //
+    // Files in packages are defined in terms of `package:` URIs, while
+    // non-package URIs are defined using the `dart-dev-app` scheme.
+    String rootScheme = module.isSdk ? 'dart-dev-sdk' : 'dev-dart-app';
+    String sourceToImportUri(Uri relativeUri) {
+      if (module.isPackage) {
+        var basePath = module.packageBase.path;
+        var packageRelativePath = basePath == "./"
+            ? relativeUri.path
+            : relativeUri.path.substring(basePath.length);
+        return 'package:${module.name}/$packageRelativePath';
+      } else {
+        return '$rootScheme:/$relativeUri';
+      }
+    }
+
+    // We create both a .packages and package_config.json file which defines
+    // the location of this module if it is a package.  The CFE requires that
+    // if a `package:` URI of a dependency is used in an import, then we need
+    // that package entry in the associated file. However, after it checks that
+    // the definition exists, the CFE will not actually use the resolved URI if
+    // a library for the import URI is already found in one of the provide
+    // .dill files of the dependencies. For that reason, and to ensure that
+    // a step only has access to the files provided in a module, we generate a
+    // config file with invalid folders for other packages.
+    // TODO(sigmund): follow up with the CFE to see if we can remove the need
+    // for these dummy entries..
+    // TODO(joshualitt): Generate just the json file.
+    var packagesJson = [];
+    var packagesContents = StringBuffer();
+    if (module.isPackage) {
+      packagesContents.write('${module.name}:${module.packageBase}\n');
+      packagesJson.add(_packageConfigEntry(
+          module.name, Uri.parse('../${module.packageBase}')));
+    }
+
+    Set<Module> transitiveDependencies = computeTransitiveDependencies(module);
+    int unusedNum = 0;
+    for (Module dependency in transitiveDependencies) {
+      if (dependency.isPackage) {
+        // rootUri should be ignored for dependent modules, so we pass in a
+        // bogus value.
+        var rootUri = Uri.parse('unused$unusedNum');
+        unusedNum++;
+
+        var dependentPackage = packageConfig[dependency.name];
+        var packageJson = dependentPackage == null
+            ? _packageConfigEntry(dependency.name, rootUri)
+            : _packageConfigEntry(dependentPackage.name, rootUri,
+                version: dependentPackage.languageVersion);
+        packagesJson.add(packageJson);
+        packagesContents.write('${dependency.name}:$rootUri\n');
+      }
+    }
+
+    if (module.isPackage) {
+      await File.fromUri(root.resolve(packageConfigJsonPath))
+          .create(recursive: true);
+      await File.fromUri(root.resolve(packageConfigJsonPath)).writeAsString('{'
+          '  "configVersion": ${packageConfig.version},'
+          '  "packages": [ ${packagesJson.join(',')} ]'
+          '}');
+    }
+
+    await File.fromUri(root.resolve('.packages'))
+        .writeAsString('$packagesContents');
+
+    List<String> sources;
+    List<String> extraArgs = ['--packages-file', '$rootScheme:/.packages'];
+    if (module.isSdk) {
+      // When no flags are passed, we can skip compilation and reuse the
+      // platform.dill created by build.py.
+      if (flags.isEmpty) {
+        var platform = computePlatformBinariesLocation()
+            .resolve("dart2js_platform_unsound.dill");
+        var destination = root.resolveUri(toUri(module, outputData));
+        if (_options.verbose) {
+          print('command:\ncp $platform $destination');
+        }
+        await File.fromUri(platform).copy(destination.toFilePath());
+        return;
+      }
+      sources = requiredLibraries['dart2js'] + ['dart:core'];
+      extraArgs += [
+        '--libraries-file',
+        '$rootScheme:///sdk/lib/libraries.json'
+      ];
+      assert(transitiveDependencies.isEmpty);
+    } else {
+      sources = module.sources.map(sourceToImportUri).toList();
+    }
+
+    // TODO(joshualitt): Ensure the kernel worker has some way to specify
+    // --no-sound-null-safety
+    List<String> args = [
+      _kernelWorkerScript,
+      ...stepArguments,
+      '--exclude-non-sources',
+      '--multi-root',
+      '$root',
+      '--multi-root-scheme',
+      rootScheme,
+      ...extraArgs,
+      '--output',
+      '${toUri(module, outputData)}',
+      ...(transitiveDependencies
+          .expand((m) => ['--input-summary', '${toUri(m, inputData)}'])),
+      ...(sources.expand((String uri) => ['--source', uri])),
+      ...(flags.expand((String flag) => ['--enable-experiment', flag])),
+    ];
+
+    var result =
+        await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
+    _checkExitCode(result, this, module);
+  }
+
+  List<String> get stepArguments;
+
+  DataId get inputData;
+
+  DataId get outputData;
+
+  @override
+  void notifyCached(Module module) {
+    if (_options.verbose) print("\ncached step: $stepName on $module");
+  }
+}
+
+// Step that compiles sources in a module to a summary .dill file.
+class OutlineDillCompilationStep extends CFEStep {
+  @override
+  List<DataId> get resultData => const [dillSummaryId];
+
+  @override
+  bool get needsSources => true;
+
+  @override
+  List<DataId> get dependencyDataNeeded => const [dillSummaryId];
+
+  @override
+  List<DataId> get moduleDataNeeded => const [];
+
+  @override
+  List<String> get stepArguments =>
+      ['--target', 'dart2js_summary', '--summary-only'];
+
+  @override
+  DataId get inputData => dillSummaryId;
+
+  @override
+  DataId get outputData => dillSummaryId;
+
+  OutlineDillCompilationStep() : super('outline-dill-compilation');
+}
+
+// Step that compiles sources in a module to a .dill file.
+class FullDillCompilationStep extends CFEStep {
+  @override
+  List<DataId> get resultData => const [dillId];
+
+  @override
+  bool get needsSources => true;
+
+  @override
+  List<DataId> get dependencyDataNeeded => const [dillSummaryId];
+
+  @override
+  List<DataId> get moduleDataNeeded => const [];
+
+  @override
+  List<String> get stepArguments =>
+      ['--target', 'dart2js', '--no-summary', '--no-summary-only'];
+
+  @override
+  DataId get inputData => dillSummaryId;
+
+  @override
+  DataId get outputData => dillId;
+
+  FullDillCompilationStep() : super('full-dill-compilation');
+}
+
+class ModularAnalysisStep implements IOModularStep {
+  @override
+  List<DataId> get resultData => const [modularDataId, modularUpdatedDillId];
+
+  @override
+  bool get needsSources => false;
+
+  @override
+  List<DataId> get dependencyDataNeeded => const [dillId];
+
+  @override
+  List<DataId> get moduleDataNeeded => const [dillId];
+
+  @override
+  bool get onlyOnMain => false;
+
+  @override
+  Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
+      List<String> flags) async {
+    if (_options.verbose) print("\nstep: modular analysis on $module");
+    Set<Module> transitiveDependencies = computeTransitiveDependencies(module);
+    Iterable<String> dillDependencies =
+        transitiveDependencies.map((m) => '${toUri(m, dillId)}');
+    List<String> args = [
+      '--packages=${sdkRoot.toFilePath()}/.packages',
+      _dart2jsScript,
+      if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
+      '${Flags.inputDill}=${toUri(module, dillId)}',
+      if (dillDependencies.isNotEmpty)
+        '--dill-dependencies=${dillDependencies.join(',')}',
+      '--out=${toUri(module, modularUpdatedDillId)}',
+      '${Flags.writeModularAnalysis}=${toUri(module, modularDataId)}',
+      for (String flag in flags) '--enable-experiment=$flag',
+    ];
+    var result =
+        await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
+
+    _checkExitCode(result, this, module);
+  }
+
+  @override
+  void notifyCached(Module module) {
+    if (_options.verbose) {
+      print("cached step: dart2js modular analysis on $module");
+    }
+  }
+}
+
+DataId idForDill({bool useModularAnalysis}) =>
+    useModularAnalysis ? modularUpdatedDillId : dillId;
+
+List<DataId> inputFromAnalysis({bool useModularAnalysis = false}) => [
+      idForDill(useModularAnalysis: useModularAnalysis),
+      if (useModularAnalysis) modularDataId,
+    ];
+
+// Step that invokes the dart2js closed world computation.
+class ComputeClosedWorldStep implements IOModularStep {
+  final bool useModularAnalysis;
+
+  ComputeClosedWorldStep({this.useModularAnalysis});
+
+  @override
+  List<DataId> get resultData => const [closedWorldId, globalUpdatedDillId];
+
+  @override
+  bool get needsSources => false;
+
+  @override
+  List<DataId> get dependencyDataNeeded =>
+      inputFromAnalysis(useModularAnalysis: useModularAnalysis);
+
+  @override
+  List<DataId> get moduleDataNeeded =>
+      inputFromAnalysis(useModularAnalysis: useModularAnalysis);
+
+  @override
+  bool get onlyOnMain => true;
+
+  @override
+  Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
+      List<String> flags) async {
+    if (_options.verbose)
+      print("\nstep: dart2js compute closed world on $module");
+    Set<Module> transitiveDependencies = computeTransitiveDependencies(module);
+    DataId dillId = idForDill(useModularAnalysis: useModularAnalysis);
+    Iterable<String> dillDependencies =
+        transitiveDependencies.map((m) => '${toUri(m, dillId)}');
+    List<String> dataDependencies = transitiveDependencies
+        .map((m) => '${toUri(m, modularDataId)}')
+        .toList();
+    dataDependencies.add('${toUri(module, modularDataId)}');
+    List<String> args = [
+      '--packages=${sdkRoot.toFilePath()}/.packages',
+      _dart2jsScript,
+      // TODO(sigmund): remove this dependency on libraries.json
+      if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
+      '${Flags.entryUri}=$fakeRoot${module.mainSource}',
+      '${Flags.inputDill}=${toUri(module, dillId)}',
+      for (String flag in flags) '--enable-experiment=$flag',
+      '${Flags.dillDependencies}=${dillDependencies.join(',')}',
+      if (useModularAnalysis)
+        '${Flags.readModularAnalysis}=${dataDependencies.join(',')}',
+      '${Flags.writeClosedWorld}=${toUri(module, closedWorldId)}',
+      Flags.noClosedWorldInData,
+      '--out=${toUri(module, globalUpdatedDillId)}',
+    ];
+    var result =
+        await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
+
+    _checkExitCode(result, this, module);
+  }
+
+  @override
+  void notifyCached(Module module) {
+    if (_options.verbose)
+      print("\ncached step: dart2js compute closed world on $module");
+  }
+}
+
+// Step that runs the dart2js modular analysis.
+class GlobalAnalysisStep implements IOModularStep {
+  @override
+  List<DataId> get resultData => const [globalDataId];
+
+  @override
+  bool get needsSources => false;
+
+  @override
+  List<DataId> get dependencyDataNeeded => const [globalUpdatedDillId];
+
+  @override
+  List<DataId> get moduleDataNeeded =>
+      const [closedWorldId, globalUpdatedDillId];
+
+  @override
+  bool get onlyOnMain => true;
+
+  @override
+  Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
+      List<String> flags) async {
+    if (_options.verbose) print("\nstep: dart2js global analysis on $module");
+    List<String> args = [
+      '--packages=${sdkRoot.toFilePath()}/.packages',
+      _dart2jsScript,
+      // TODO(sigmund): remove this dependency on libraries.json
+      if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
+      '${Flags.entryUri}=$fakeRoot${module.mainSource}',
+      '${Flags.inputDill}=${toUri(module, globalUpdatedDillId)}',
+      for (String flag in flags) '--enable-experiment=$flag',
+      '${Flags.readClosedWorld}=${toUri(module, closedWorldId)}',
+      '${Flags.writeData}=${toUri(module, globalDataId)}',
+      // TODO(joshualitt): delete this flag after google3 roll
+      '${Flags.noClosedWorldInData}',
+    ];
+    var result =
+        await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
+
+    _checkExitCode(result, this, module);
+  }
+
+  @override
+  void notifyCached(Module module) {
+    if (_options.verbose)
+      print("\ncached step: dart2js global analysis on $module");
+  }
+}
+
+// Step that invokes the dart2js code generation on the main module given the
+// results of the global analysis step and produces one shard of the codegen
+// output.
+class Dart2jsCodegenStep implements IOModularStep {
+  final ShardDataId codeId;
+
+  Dart2jsCodegenStep(this.codeId);
+
+  @override
+  List<DataId> get resultData => [codeId];
+
+  @override
+  bool get needsSources => false;
+
+  @override
+  List<DataId> get dependencyDataNeeded => const [];
+
+  @override
+  List<DataId> get moduleDataNeeded =>
+      const [globalUpdatedDillId, closedWorldId, globalDataId];
+
+  @override
+  bool get onlyOnMain => true;
+
+  @override
+  Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
+      List<String> flags) async {
+    if (_options.verbose) print("\nstep: dart2js backend on $module");
+    List<String> args = [
+      '--packages=${sdkRoot.toFilePath()}/.packages',
+      _dart2jsScript,
+      if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
+      '${Flags.entryUri}=$fakeRoot${module.mainSource}',
+      '${Flags.inputDill}=${toUri(module, globalUpdatedDillId)}',
+      for (String flag in flags) '--enable-experiment=$flag',
+      '${Flags.readClosedWorld}=${toUri(module, closedWorldId)}',
+      '${Flags.readData}=${toUri(module, globalDataId)}',
+      '${Flags.writeCodegen}=${toUri(module, codeId.dataId)}',
+      '${Flags.codegenShard}=${codeId.shard}',
+      '${Flags.codegenShards}=${codeId.dataId.shards}',
+    ];
+    var result =
+        await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
+
+    _checkExitCode(result, this, module);
+  }
+
+  @override
+  void notifyCached(Module module) {
+    if (_options.verbose) print("cached step: dart2js backend on $module");
+  }
+}
+
+// Step that invokes the dart2js codegen enqueuer and emitter on the main module
+// given the results of the global analysis step and codegen shards.
+class Dart2jsEmissionStep implements IOModularStep {
+  @override
+  List<DataId> get resultData => const [jsId];
+
+  @override
+  bool get needsSources => false;
+
+  @override
+  List<DataId> get dependencyDataNeeded => const [];
+
+  @override
+  List<DataId> get moduleDataNeeded => const [
+        globalUpdatedDillId,
+        closedWorldId,
+        globalDataId,
+        codeId0,
+        codeId1
+      ];
+
+  @override
+  bool get onlyOnMain => true;
+
+  @override
+  Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
+      List<String> flags) async {
+    if (_options.verbose) print("step: dart2js backend on $module");
+    List<String> args = [
+      '--packages=${sdkRoot.toFilePath()}/.packages',
+      _dart2jsScript,
+      if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
+      '${Flags.entryUri}=$fakeRoot${module.mainSource}',
+      '${Flags.inputDill}=${toUri(module, globalUpdatedDillId)}',
+      for (String flag in flags) '${Flags.enableLanguageExperiments}=$flag',
+      '${Flags.readClosedWorld}=${toUri(module, closedWorldId)}',
+      '${Flags.readData}=${toUri(module, globalDataId)}',
+      '${Flags.readCodegen}=${toUri(module, codeId)}',
+      '${Flags.codegenShards}=${codeId.shards}',
+      '--out=${toUri(module, jsId)}',
+    ];
+    var result =
+        await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
+
+    _checkExitCode(result, this, module);
+  }
+
+  @override
+  void notifyCached(Module module) {
+    if (_options.verbose) print("\ncached step: dart2js backend on $module");
+  }
+}
+
+/// Step that runs the output of dart2js in d8 and saves the output.
+class RunD8 implements IOModularStep {
+  @override
+  List<DataId> get resultData => const [txtId];
+
+  @override
+  bool get needsSources => false;
+
+  @override
+  List<DataId> get dependencyDataNeeded => const [];
+
+  @override
+  List<DataId> get moduleDataNeeded => const [jsId];
+
+  @override
+  bool get onlyOnMain => true;
+
+  @override
+  Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
+      List<String> flags) async {
+    if (_options.verbose) print("\nstep: d8 on $module");
+    List<String> d8Args = [
+      sdkRoot
+          .resolve('sdk/lib/_internal/js_runtime/lib/preambles/d8.js')
+          .toFilePath(),
+      root.resolveUri(toUri(module, jsId)).toFilePath(),
+    ];
+    var result = await _runProcess(
+        sdkRoot.resolve(_d8executable).toFilePath(), d8Args, root.toFilePath());
+
+    _checkExitCode(result, this, module);
+
+    await File.fromUri(root.resolveUri(toUri(module, txtId)))
+        .writeAsString(result.stdout);
+  }
+
+  @override
+  void notifyCached(Module module) {
+    if (_options.verbose) print("\ncached step: d8 on $module");
+  }
+}
+
+void _checkExitCode(ProcessResult result, IOModularStep step, Module module) {
+  if (result.exitCode != 0 || _options.verbose) {
+    stdout.write(result.stdout);
+    stderr.write(result.stderr);
+  }
+  if (result.exitCode != 0) {
+    throw "${step.runtimeType} failed on $module:\n\n"
+        "stdout:\n${result.stdout}\n\n"
+        "stderr:\n${result.stderr}";
+  }
+}
+
+Future<ProcessResult> _runProcess(
+    String command, List<String> arguments, String workingDirectory) {
+  if (_options.verbose) {
+    print('command:\n$command ${arguments.join(' ')} from $workingDirectory');
+  }
+  return Process.run(command, arguments, workingDirectory: workingDirectory);
+}
+
+String get _d8executable {
+  if (Platform.isWindows) {
+    return 'third_party/d8/windows/d8.exe';
+  } else if (Platform.isLinux) {
+    return 'third_party/d8/linux/d8';
+  } else if (Platform.isMacOS) {
+    return 'third_party/d8/macos/d8';
+  }
+  throw UnsupportedError('Unsupported platform.');
+}
+
+class ShardsDataId implements DataId {
+  @override
+  final String name;
+  final int shards;
+
+  const ShardsDataId(this.name, this.shards);
+
+  @override
+  String toString() => name;
+}
+
+class ShardDataId implements DataId {
+  final ShardsDataId dataId;
+  final int _shard;
+
+  const ShardDataId(this.dataId, this._shard);
+
+  int get shard {
+    assert(0 <= _shard && _shard < dataId.shards);
+    return _shard;
+  }
+
+  @override
+  String get name => '${dataId.name}${shard}';
+
+  @override
+  String toString() => name;
+}
+
+Future<void> resolveScripts(Options options) async {
+  Future<String> resolve(
+      String sourceUriOrPath, String relativeSnapshotPath) async {
+    Uri sourceUri = sdkRoot.resolve(sourceUriOrPath);
+    String result =
+        sourceUri.scheme == 'file' ? sourceUri.toFilePath() : sourceUriOrPath;
+    if (_options.useSdk) {
+      String snapshot = Uri.file(Platform.resolvedExecutable)
+          .resolve(relativeSnapshotPath)
+          .toFilePath();
+      if (await File(snapshot).exists()) {
+        return snapshot;
+      }
+    }
+    return result;
+  }
+
+  _options = options;
+  _dart2jsScript = await resolve(
+      'package:compiler/src/dart2js.dart', 'snapshots/dart2js.dart.snapshot');
+  _kernelWorkerScript = await resolve('utils/bazel/kernel_worker.dart',
+      'snapshots/kernel_worker.dart.snapshot');
+}
+
+String _librarySpecForSnapshot = Uri.file(Platform.resolvedExecutable)
+    .resolve('../lib/libraries.json')
+    .toFilePath();
diff --git a/pkg/compiler/tool/modular_test_suite_legacy.dart b/pkg/compiler/tool/modular_test_suite_legacy.dart
new file mode 100644
index 0000000..875a64a
--- /dev/null
+++ b/pkg/compiler/tool/modular_test_suite_legacy.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Test the modular compilation pipeline of dart2js.
+///
+/// This is a shell that runs multiple tests, one per folder under `data/`.
+import 'dart:async';
+
+import 'package:modular_test/src/io_pipeline.dart';
+import 'package:modular_test/src/runner.dart';
+import 'modular_test_suite_helper.dart';
+
+main(List<String> args) async {
+  var options = Options.parse(args);
+  await resolveScripts(options);
+  await Future.wait([
+    runSuite(
+        sdkRoot.resolve('tests/modular/'),
+        'tests/modular',
+        options,
+        IOPipeline([
+          OutlineDillCompilationStep(),
+          FullDillCompilationStep(),
+          ComputeClosedWorldStep(useModularAnalysis: false),
+          GlobalAnalysisStep(),
+          Dart2jsCodegenStep(codeId0),
+          Dart2jsCodegenStep(codeId1),
+          Dart2jsEmissionStep(),
+          RunD8(),
+        ], cacheSharedModules: true)),
+  ]);
+}
diff --git a/tools/VERSION b/tools/VERSION
index 64cee58..30279d0 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 16
 PATCH 0
-PRERELEASE 59
+PRERELEASE 60
 PRERELEASE_PATCH 0
\ No newline at end of file
diff --git a/tools/bots/test_matrix.json b/tools/bots/test_matrix.json
index 2396b82..c060e70 100644
--- a/tools/bots/test_matrix.json
+++ b/tools/bots/test_matrix.json
@@ -424,6 +424,17 @@
         "use-sdk": true
       }
     },
+    "web-unittest-asserts-legacy-(linux|mac|win)": {
+      "options": {
+        "builder-tag": "web-legacy",
+        "compiler": "dartk",
+        "enable-asserts": true,
+        "mode": "release",
+        "runtime": "vm",
+        "timeout": 240,
+        "use-sdk": true
+      }
+    },
     "unittest-asserts-(debug|product|release)-(linux|mac|win)": {
       "options": {
         "compiler": "dartk",
@@ -2401,6 +2412,17 @@
           ]
         },
         {
+          "name": "dart2js legacy modular tests",
+          "script": "out/ReleaseX64/dart-sdk/bin/dart",
+          "testRunner": true,
+          "arguments": [
+            "pkg/compiler/tool/modular_test_suite_legacy.dart",
+            "-nweb-unittest-asserts-legacy-linux",
+            "--verbose",
+            "--use-sdk"
+          ]
+        },
+        {
           "name": "dart2js modular tests",
           "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "testRunner": true,