Add infrastructure and tests for evaluation with frontend_server (#902)

- Create new non-public package:frontend_server_common
  - logic is a fork of the Frontend Server compiler logic from Flutter
  - Long term this package will contain a shared set of logic for use from package:dwds, package:webdev and package:flutter_tools
- Add new tests to stress the Frontend Server compiler
  - Tag those tests as `frontend-server` and skip for now
  - Waiting for https://dart-review.googlesource.com/c/sdk/+/138010 to land in the dev channel
- Update `modules.dart` to handle both compilers (Frontend Server, package:build*)
  - As this directly impacts breakpoint logic, split these tests out and make them explicit
diff --git a/.travis.yml b/.travis.yml
index f93d56d..2938fcd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,10 +16,10 @@
 jobs:
   include:
     - stage: analyzer_and_format
-      name: "SDK: 2.6.0; PKGS: dwds, example, webdev; TASKS: `dartanalyzer --fatal-warnings .`"
+      name: "SDK: 2.6.0; PKGS: dwds, example, frontend_server_common, webdev; TASKS: `dartanalyzer --fatal-warnings .`"
       dart: "2.6.0"
       os: linux
-      env: PKGS="dwds example webdev"
+      env: PKGS="dwds example frontend_server_common webdev"
       script: ./tool/travis.sh dartanalyzer_1
     - stage: analyzer_and_format
       name: "SDK: dev; PKG: dwds; TASKS: [`dartfmt -n --set-exit-if-changed .`, `dartanalyzer --fatal-infos --fatal-warnings .`, `pub run test test/build/ensure_version_test.dart`]"
@@ -28,17 +28,17 @@
       env: PKGS="dwds"
       script: ./tool/travis.sh dartfmt dartanalyzer_0 test_0
     - stage: analyzer_and_format
-      name: "SDK: dev; PKG: example; TASKS: [`dartfmt -n --set-exit-if-changed .`, `dartanalyzer --fatal-infos --fatal-warnings .`]"
+      name: "SDK: dev; PKGS: example, frontend_server_common; TASKS: [`dartfmt -n --set-exit-if-changed .`, `dartanalyzer --fatal-infos --fatal-warnings .`]"
       dart: dev
       os: linux
-      env: PKGS="example"
+      env: PKGS="example frontend_server_common"
       script: ./tool/travis.sh dartfmt dartanalyzer_0
     - stage: analyzer_and_format
       name: "SDK: dev; PKG: webdev; TASKS: [`dartfmt -n --set-exit-if-changed .`, `dartanalyzer --fatal-infos --fatal-warnings .`, `pub run test test/build/ensure_build_test.dart`]"
       dart: dev
       os: linux
       env: PKGS="webdev"
-      script: ./tool/travis.sh dartfmt dartanalyzer_0 test_3
+      script: ./tool/travis.sh dartfmt dartanalyzer_0 test_2
     - stage: unit_test
       name: "SDK: 2.6.0; PKG: dwds; TASKS: `pub run test -x frontend-server`"
       dart: "2.6.0"
@@ -46,35 +46,35 @@
       env: PKGS="dwds"
       script: ./tool/travis.sh test_1
     - stage: unit_test
-      name: "SDK: 2.6.0; PKG: dwds; TASKS: `pub run test`"
+      name: "SDK: 2.6.0; PKG: dwds; TASKS: `pub run test -x frontend-server`"
       dart: "2.6.0"
       os: windows
       env: PKGS="dwds"
-      script: ./tool/travis.sh test_2
+      script: ./tool/travis.sh test_1
     - stage: unit_test
-      name: "SDK: dev; PKG: dwds; TASKS: `pub run test`"
+      name: "SDK: dev; PKG: dwds; TASKS: `pub run test -x frontend-server`"
       dart: dev
       os: linux
       env: PKGS="dwds"
-      script: ./tool/travis.sh test_2
+      script: ./tool/travis.sh test_1
     - stage: unit_test
       name: "SDK: 2.6.0; PKG: webdev; TASKS: `pub run test -j 1`"
       dart: "2.6.0"
       os: linux
       env: PKGS="webdev"
-      script: ./tool/travis.sh test_4
+      script: ./tool/travis.sh test_3
     - stage: unit_test
       name: "SDK: 2.6.0; PKG: webdev; TASKS: `pub run test -j 1`"
       dart: "2.6.0"
       os: windows
       env: PKGS="webdev"
-      script: ./tool/travis.sh test_4
+      script: ./tool/travis.sh test_3
     - stage: unit_test
       name: "SDK: dev; PKG: webdev; TASKS: `pub run test -j 1`"
       dart: dev
       os: linux
       env: PKGS="webdev"
-      script: ./tool/travis.sh test_4
+      script: ./tool/travis.sh test_3
 
 stages:
   - analyzer_and_format
diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md
index 26ba7c5..f7d85e2 100644
--- a/dwds/CHANGELOG.md
+++ b/dwds/CHANGELOG.md
@@ -1,6 +1,6 @@
 ## 2.0.0-dev
 
-- Depend on the latest `package:vm_service` version `2.3.1`.
+- Depend on the latest `package:vm_service` version `3.0.0+1`.
 
 **Breaking Changes:**
 - Now require a `LoadStrategy` to `Dwds.start`. This package defines two
diff --git a/dwds/dart_test.yaml b/dwds/dart_test.yaml
index 479dd58..72d335d 100644
--- a/dwds/dart_test.yaml
+++ b/dwds/dart_test.yaml
@@ -3,3 +3,4 @@
 
 tags:
   extension:  # Extension tests require configuration, so we may exclude.
+  frontend-server: #Frontend server test require dev version of SDK, so we may exclude for stable SDK
diff --git a/dwds/lib/src/debugging/execution_context.dart b/dwds/lib/src/debugging/execution_context.dart
index 117843a..868f62a 100644
--- a/dwds/lib/src/debugging/execution_context.dart
+++ b/dwds/lib/src/debugging/execution_context.dart
@@ -7,8 +7,13 @@
 import 'package:async/async.dart';
 import 'package:dwds/src/debugging/remote_debugger.dart';
 
+abstract class ExecutionContext {
+  /// Returns the context ID that contains the running Dart application.
+  Future<int> get id;
+}
+
 /// The execution context in which to do remote evaluations.
-class ExecutionContext {
+class RemoteDebuggerExecutionContext extends ExecutionContext {
   final RemoteDebugger _remoteDebugger;
 
   // Contexts that may contain a Dart application.
@@ -16,7 +21,7 @@
 
   int _id;
 
-  /// Returns the context ID that contains the running Dart application.
+  @override
   Future<int> get id async {
     if (_id != null) return _id;
     while (await _contexts.hasNext
@@ -43,7 +48,7 @@
     return _id;
   }
 
-  ExecutionContext(this._id, this._remoteDebugger) {
+  RemoteDebuggerExecutionContext(this._id, this._remoteDebugger) {
     var contextController = StreamController<int>();
     _remoteDebugger
         .eventStream('Runtime.executionContextsCleared', (e) => e)
diff --git a/dwds/lib/src/debugging/location.dart b/dwds/lib/src/debugging/location.dart
index 3969555..4dda704 100644
--- a/dwds/lib/src/debugging/location.dart
+++ b/dwds/lib/src/debugging/location.dart
@@ -153,7 +153,6 @@
   /// Returns all [Location] data for a provided JS scriptId.
   Future<Set<Location>> locationsForJs(String scriptId) async {
     var module = await _modules.moduleForScriptId(scriptId);
-
     var cache = _scriptIdToLocation[scriptId];
     if (cache != null) return cache;
 
@@ -231,12 +230,17 @@
     var moduleExtension = await _modules.moduleExtension;
     var modulePath = '$module$moduleExtension';
     if (modulePath.endsWith('dart_sdk.js') ||
-        modulePath.endsWith('dart_sdk.ddk.js')) {
+        modulePath.endsWith('dart_sdk.ddk.js') ||
+        // .lib.js extensions come from frontend server
+        modulePath.endsWith('dart_sdk.lib.js')) {
       return result;
     }
+
+    modulePath = _modules.adjustForRoot(modulePath);
+    var scriptLocation = p.url.dirname(modulePath);
+
     var sourceMapContents =
         await _assetReader.sourceMapContents('$modulePath.map');
-    var scriptLocation = p.url.dirname('/$modulePath');
     if (sourceMapContents == null) return result;
     var scriptId = await _modules.scriptIdForModule(module);
     if (scriptId == null) return result;
@@ -255,7 +259,8 @@
           var relativeSegments = p.split(mapping.urls[index]);
           var path = p.url
               .normalize(p.url.joinAll([scriptLocation, ...relativeSegments]));
-          var dartUri = DartUri(path, _root);
+          var uri = _modules.adjustForRoot(path);
+          var dartUri = DartUri('/$uri', _root);
           result.add(Location.from(
             scriptId,
             lineEntry,
diff --git a/dwds/lib/src/debugging/modules.dart b/dwds/lib/src/debugging/modules.dart
index c2d4980..c3300d1 100644
--- a/dwds/lib/src/debugging/modules.dart
+++ b/dwds/lib/src/debugging/modules.dart
@@ -20,7 +20,8 @@
   // The Dart server path to containing module.
   final _sourceToModule = <String, String>{};
   // The Dart server path to library import uri
-  final _sourceToLibrary = <String, String>{};
+  final _sourceToLibrary = <String, Uri>{};
+  Completer<bool> _moduleCompleter = Completer<bool>();
 
   // The Chrome script ID to corresponding module.
   final _scriptIdToModule = <String, String>{};
@@ -30,7 +31,8 @@
 
   final _moduleExtensionCompleter = Completer<String>();
 
-  Modules(this._remoteDebugger, this._root, this._executionContext);
+  Modules(this._remoteDebugger, String root, this._executionContext)
+      : _root = root == '' ? '/' : root;
 
   /// Completes with the module extension i.e. `.ddc.js` or `.ddk.js`.
   ///
@@ -47,6 +49,8 @@
     // across hot reloads.
     _sourceToModule.clear();
     _sourceToLibrary.clear();
+    _moduleCompleter = Completer<bool>();
+    _initializeMapping();
   }
 
   /// Returns the module for the Chrome script ID.
@@ -59,26 +63,19 @@
 
   /// Returns the containing module for the provided Dart server path.
   Future<String> moduleForSource(String serverPath) async {
-    if (_sourceToModule.isEmpty) {
-      await _initializeMapping();
-    }
+    await _moduleCompleter.future;
     return _sourceToModule[serverPath];
   }
 
   /// Returns the containing library importUri for the provided Dart server path.
-  Future<String> libraryForSource(String serverPath) async {
-    if (_sourceToLibrary.isEmpty) {
-      await _initializeMapping();
-    }
+  Future<Uri> libraryForSource(String serverPath) async {
+    await _moduleCompleter.future;
     return _sourceToLibrary[serverPath];
   }
 
   // Returns mapping from server paths to library paths
   Future<Map<String, String>> modules() async {
-    if (_sourceToModule.isEmpty) {
-      await _initializeMapping();
-    }
-
+    await _moduleCompleter.future;
     return _sourceToModule;
   }
 
@@ -86,7 +83,9 @@
   Future<Null> noteModule(String url, String scriptId) async {
     var path = Uri.parse(url).path;
     if (path == null ||
-        !(path.endsWith('.ddc.js') || path.endsWith('.ddk.js'))) {
+        !(path.endsWith('.ddc.js') ||
+            path.endsWith('.ddk.js') ||
+            path.endsWith('.lib.js'))) {
       return;
     }
 
@@ -94,21 +93,77 @@
     if (!_moduleExtensionCompleter.isCompleted) {
       if (path.endsWith('.ddc.js')) {
         _moduleExtensionCompleter.complete('.ddc.js');
-      } else {
+      } else if (path.endsWith('.ddk.js')) {
         _moduleExtensionCompleter.complete('.ddk.js');
+      } else {
+        _moduleExtensionCompleter.complete('.lib.js');
       }
     }
 
-    var module =
-        // Remove the DDC extension (e.g. .ddc.js) from the path.
-        _moduleFor(p.withoutExtension(p.withoutExtension(path)));
+    // TODO(annagrin): redirect modulePath->moduleName query to load strategy
+    //
+    // The code below is trying to guess the module name from js module path
+    // by assuming we can find a dart server path with a matching name located
+    // at the same server directory. Then it uses source->moduleName map to get
+    // the module name.
+    // [issue #917](https://github.com/dart-lang/webdev/issues/917)
+    // [issue #910](https://github.com/dart-lang/webdev/issues/910)
+    var serverPath = _jsModulePathToServerPath(path);
+    var module = await moduleForSource(serverPath);
 
     _scriptIdToModule[scriptId] = module;
     _moduleToScriptId[module] = scriptId;
   }
 
+  String _jsModulePathToServerPath(String path) {
+    // remove extensions, such as '.ddc.js'
+    var serverPath = p.withoutExtension(p.withoutExtension(path));
+    // server path does not contain leading '/'
+    serverPath =
+        serverPath.startsWith('/') ? serverPath.substring(1) : serverPath;
+    // server path has '.dart' extension
+    serverPath = serverPath.endsWith('.dart') ? serverPath : '$serverPath.dart';
+    // server path should be relative to the asset server root
+    serverPath = adjustForRoot(serverPath);
+    return DartUri('/$serverPath', _root).serverPath;
+  }
+
+  /// Make path relative to the asset server's serving root.
+  ///
+  /// Remove the asset server root directory for non-package files, if any,
+  /// but do not remove the _root, which is the directory off the
+  /// asset server root. This is needed to produce paths that will be used
+  /// in requests to the asset server, such as dart script paths, dart
+  /// locations, or source map paths. Asset server is serving from the asset
+  /// server root directory, so it expects the requests to be relative to it.
+  // Note: This is a temporary workaround until we solve inconsistencies in
+  // different configurations by introducing module name and path translation
+  // interfaces between compiler, asset server, and the debugger.
+  // TODO(annagrin): module interface
+  // [issue #910](https://github.com/dart-lang/webdev/issues/910)
+  String adjustForRoot(String path) {
+    // path == 'dir/main.dart' => pathRoot == 'dir'
+    // path == 'main.dart' => pathRoot == '.'
+    var segments = p.split(path);
+    var pathRoot = p.split(p.dirname(path))[0];
+
+    // _root == 'http:/localhost:port/dir/index.html' => indexRoot == 'dir'
+    // _root == 'http:/localhost:port/index.html' => indexRoot == '.'
+    var indexPath = Uri.parse(_root).path.substring(1);
+    var indexRoot = p.split(p.dirname(indexPath))[0];
+
+    // remove the root from path only if not equal to packages or indexRoot
+    var result = pathRoot == 'packages' || pathRoot == indexRoot
+        // Module paths are consistent across platforms so join with a
+        // forward slash.
+        ? p.url.joinAll(segments)
+        : p.url.joinAll(segments.skip(1));
+    return result;
+  }
+
   /// Initializes [_sourceToModule].
   Future<void> _initializeMapping() async {
+    if (_moduleCompleter.isCompleted) return;
     var expression = '''
     (function() {
           var dart = ${globalLoadStrategy.loadModuleSnippet}('dart_sdk').dart;
@@ -116,7 +171,7 @@
           dart.getModuleNames().forEach(function(module){
             Object.keys(dart.getModuleLibraries(module)).forEach(
               function(script){
-                result[script] =  '/' + module;
+                result[script] = module;
             });
           });
           return result;
@@ -133,10 +188,20 @@
     for (var dartScript in value.keys) {
       if (!dartScript.endsWith('.dart')) continue;
       var serverPath = DartUri(dartScript, _root).serverPath;
-      _sourceToModule[serverPath] = _moduleFor(value[dartScript] as String,
-          skipRoot: dartScript.startsWith('org-dartlang-app:///'));
-      _sourceToLibrary[serverPath] = Uri.parse(dartScript).path;
+
+      // get module name from module Uri
+      // Note: This is a temporary workaround until we solve inconsistencies
+      // in different configurations by introducing module name and path
+      // translation interfaces between compiler, asset server, and the
+      // debugger.
+      // TODO(annagrin): module interface
+      // [issue #910](https://github.com/dart-lang/webdev/issues/910)
+      var moduleUri = Uri.parse(value[dartScript] as String);
+      var module = _moduleFor(moduleUri.path);
+      _sourceToModule[serverPath] = module;
+      _sourceToLibrary[serverPath] = Uri.parse(dartScript);
     }
+    _moduleCompleter.complete();
   }
 
   /// Returns the module for the provided path.
@@ -147,6 +212,7 @@
   ///   some/root/bar/module
   ///
   String _moduleFor(String path, {bool skipRoot}) {
+    path = '/$path';
     skipRoot ??= false;
     var result = '';
     if (path.contains('/packages/')) {
diff --git a/dwds/lib/src/handlers/dev_handler.dart b/dwds/lib/src/handlers/dev_handler.dart
index 0b05093..27585c6 100644
--- a/dwds/lib/src/handlers/dev_handler.dart
+++ b/dwds/lib/src/handlers/dev_handler.dart
@@ -137,7 +137,7 @@
         var evaluatedAppId = result.result['result']['value'];
         if (evaluatedAppId == appInstanceId) {
           appTab = tab;
-          executionContext = ExecutionContext(
+          executionContext = RemoteDebuggerExecutionContext(
               context, WebkitDebugger(WipDebugger(tabConnection)));
           break;
         }
diff --git a/dwds/lib/src/loaders/frontend_server_require.dart b/dwds/lib/src/loaders/frontend_server_require.dart
new file mode 100644
index 0000000..e5bfcd6
--- /dev/null
+++ b/dwds/lib/src/loaders/frontend_server_require.dart
@@ -0,0 +1,44 @@
+// Copyright 2020 The Dart 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:dwds/dwds.dart';
+
+/// Provides a [RequireStrategy] suitable for use with Frontend Server.
+class FrontendServerRequireStrategyProvider {
+  final ReloadConfiguration _configuration;
+  final Iterable<String> _modules;
+  RequireStrategy _requireStrategy;
+
+  FrontendServerRequireStrategyProvider(this._modules, this._configuration);
+
+  RequireStrategy get strategy => _requireStrategy ??= RequireStrategy(
+      _configuration, '.lib.js', _moduleProvider, _digestsProvider);
+
+  Future<Map<String, String>> _digestsProvider(String entrypoint) async {
+    return {};
+  }
+
+  Future<Map<String, String>> _moduleProvider(String entrypoint) async {
+    final modulePaths = <String, String>{};
+    for (var module in _modules) {
+      // We are currently 'guessing' module names from js module paths,
+      // which is not reliable.
+      // example:
+      //  module: /web/main.dart.lib.js'
+      //  name: web/main.dart
+      //  path: web/main.dart.lib
+      // Note: This is a temporary workaround until we solve inconsistencies
+      // in different configurations by introducing module name and path
+      // translation interfaces between compiler, asset server, and the
+      // debugger.
+      // TODO(annagrin): module interface
+      // [issue #910](https://github.com/dart-lang/webdev/issues/910)
+      module = module.startsWith('/') ? module.substring(1) : module;
+      var name = module.replaceAll('.lib.js', '');
+      var path = module.replaceAll('.js', '');
+      modulePaths[name] = path;
+    }
+    return modulePaths;
+  }
+}
diff --git a/dwds/lib/src/servers/extension_debugger.dart b/dwds/lib/src/servers/extension_debugger.dart
index 57641d0..30e19bc 100644
--- a/dwds/lib/src/servers/extension_debugger.dart
+++ b/dwds/lib/src/servers/extension_debugger.dart
@@ -86,7 +86,8 @@
         }
       } else if (message is DevToolsRequest) {
         instanceId = message.instanceId;
-        _executionContext = ExecutionContext(message.contextId, this);
+        _executionContext =
+            RemoteDebuggerExecutionContext(message.contextId, this);
         _devToolsRequestController.sink.add(message);
       }
     }, onError: (_) {
diff --git a/dwds/lib/src/services/expression_evaluator.dart b/dwds/lib/src/services/expression_evaluator.dart
index 59540dc..2d2fbf8 100644
--- a/dwds/lib/src/services/expression_evaluator.dart
+++ b/dwds/lib/src/services/expression_evaluator.dart
@@ -106,14 +106,18 @@
     for (var serverPath in modules.keys) {
       var module = modules[serverPath];
       var library = await _modules.libraryForSource(serverPath);
-      var name = pathToJSIdentifier(library.replaceAll('.dart', ''));
+      var libraryPath = library.path;
+      if (library.scheme == 'package') {
+        libraryPath = libraryPath.split('/').skip(1).join('/');
+      }
+      var name = pathToJSIdentifier(libraryPath.replaceAll('.dart', ''));
       jsModules[name] = module;
     }
     _printTrace('Expression evaluator: js modules: $jsModules');
 
     var compilationResult = await _compiler.compileExpressionToJs(
         isolateId,
-        libraryUri,
+        libraryUri.toString(),
         dartLocation.line,
         dartLocation.column,
         jsModules,
@@ -138,15 +142,16 @@
       // [issue 40449](https://github.com/dart-lang/sdk/issues/40449)
       var error = jsExpression;
 
-      if (jsExpression.startsWith('[')) {
-        jsExpression = jsExpression.substring(1);
+      if (error.startsWith('[')) {
+        error = error.substring(1);
       }
-      if (jsExpression.endsWith(']')) {
-        jsExpression = jsExpression.substring(0, jsExpression.lastIndexOf(']'));
+      if (error.endsWith(']')) {
+        error = error.substring(0, error.lastIndexOf(']'));
       }
 
-      jsExpression.replaceAll(
-          r'org-dartlang-debug:synthetic_debug_expression:', '');
+      error = error.replaceAll(
+          RegExp('org-dartlang-debug:synthetic_debug_expression:.* Error: '),
+          '');
       return _createError('Compilation error', error);
     }
 
@@ -202,7 +207,7 @@
       }
     }
 
-    var scopeChain = frame.getScopeChain();
+    var scopeChain = List<WipScope>.from(frame.getScopeChain()).reversed;
 
     // skip library and main scope
     for (var scope in scopeChain.skip(2)) {
diff --git a/dwds/mono_pkg.yaml b/dwds/mono_pkg.yaml
index e764dcc..90daa11 100644
--- a/dwds/mono_pkg.yaml
+++ b/dwds/mono_pkg.yaml
@@ -16,8 +16,8 @@
   - unit_test:
     - test: -x frontend-server
       dart: 2.6.0
-    - test:
+    - test: -x frontend-server
       dart: dev
-    - test:
+    - test: -x frontend-server
       os: windows
       dart: 2.6.0
diff --git a/dwds/pubspec.yaml b/dwds/pubspec.yaml
index 7acbe5b..8003f2a 100644
--- a/dwds/pubspec.yaml
+++ b/dwds/pubspec.yaml
@@ -17,6 +17,7 @@
   # devtools_server indirectly depends on devtools so keep this around.
   devtools: ^0.2.0
   devtools_server: ^0.2.0
+  file: ^5.1.0
   http: ^0.12.0
   http_multi_server: ^2.0.0
   logging: ^0.11.3
@@ -34,7 +35,7 @@
   sse: ^3.2.0
   vm_service: 3.0.0+1
   web_socket_channel: ^1.0.0
-  webkit_inspection_protocol: '>=0.4.0 <0.6.0'
+  webkit_inspection_protocol: '>=0.5.0+1 <0.6.0'
 
 dev_dependencies:
   args: ^1.0.0
@@ -45,6 +46,9 @@
   build_web_compilers: '>=1.0.0 <3.0.0'
   built_value_generator: '>=6.4.0 <8.0.0'
   graphs: ^0.2.0
+  frontend_server_common:
+    path: ../frontend_server_common
+  sync_http: 0.2.0
   js: ^0.6.1
   pubspec_parse: ^0.1.5
   stream_channel: ^2.0.0
diff --git a/dwds/test/build_daemon_breakpoint_test.dart b/dwds/test/build_daemon_breakpoint_test.dart
new file mode 100644
index 0000000..470d377
--- /dev/null
+++ b/dwds/test/build_daemon_breakpoint_test.dart
@@ -0,0 +1,90 @@
+// 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.
+
+@TestOn('vm')
+import 'dart:async';
+
+import 'package:dwds/src/connections/debug_connection.dart';
+import 'package:dwds/src/services/chrome_proxy_service.dart';
+import 'package:test/test.dart';
+import 'package:vm_service/vm_service.dart';
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+
+import 'fixtures/context.dart';
+
+final context = TestContext(
+    directory: '../fixtures/_testPackage',
+    entry: '../fixtures/_testPackage/web/main.dart',
+    path: 'index.html',
+    pathToServe: 'web');
+
+ChromeProxyService get service =>
+    fetchChromeProxyService(context.debugConnection);
+WipConnection get tabConnection => context.tabConnection;
+
+void main() {
+  group('shared context', () {
+    setUpAll(() async {
+      await context.setUp();
+    });
+
+    tearDownAll(() async {
+      await context.tearDown();
+    });
+
+    group('breakpoint', () {
+      VM vm;
+      Isolate isolate;
+      ScriptList scripts;
+      ScriptRef mainScript;
+      Stream<Event> stream;
+
+      setUp(() async {
+        vm = await service.getVM();
+        isolate = await service.getIsolate(vm.isolates.first.id);
+        scripts = await service.getScripts(isolate.id);
+
+        await service.streamListen('Debug');
+        stream = service.onEvent('Debug');
+
+        mainScript = scripts.scripts
+            .firstWhere((each) => each.uri.contains('main.dart'));
+      });
+
+      tearDown(() async {
+        await service.resume(isolate.id);
+      });
+
+      test('set breakpoint', () async {
+        var line = await context.findBreakpointLine(
+            'printLocal', isolate.id, mainScript);
+        var bp = await service.addBreakpointWithScriptUri(
+            isolate.id, mainScript.uri, line);
+
+        await stream.firstWhere(
+            (Event event) => event.kind == EventKind.kPauseBreakpoint);
+
+        expect(bp, isNotNull);
+
+        // Remove breakpoint so it doesn't impact other tests.
+        await service.removeBreakpoint(isolate.id, bp.id);
+      });
+
+      test('set breakpoint again', () async {
+        var line = await context.findBreakpointLine(
+            'printLocal', isolate.id, mainScript);
+        var bp = await service.addBreakpointWithScriptUri(
+            isolate.id, mainScript.uri, line);
+
+        await stream.firstWhere(
+            (Event event) => event.kind == EventKind.kPauseBreakpoint);
+
+        expect(bp, isNotNull);
+
+        // Remove breakpoint so it doesn't impact other tests.
+        await service.removeBreakpoint(isolate.id, bp.id);
+      });
+    });
+  });
+}
diff --git a/dwds/test/build_daemon_nested_directory_breakpoint_test.dart b/dwds/test/build_daemon_nested_directory_breakpoint_test.dart
new file mode 100644
index 0000000..0e51764
--- /dev/null
+++ b/dwds/test/build_daemon_nested_directory_breakpoint_test.dart
@@ -0,0 +1,90 @@
+// 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.
+
+@TestOn('vm')
+import 'dart:async';
+
+import 'package:dwds/src/connections/debug_connection.dart';
+import 'package:dwds/src/services/chrome_proxy_service.dart';
+import 'package:test/test.dart';
+import 'package:vm_service/vm_service.dart';
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+
+import 'fixtures/context.dart';
+
+final context = TestContext(
+    directory: '../fixtures/_test',
+    entry: '../fixtures/_test/example/hello_world/main.dart',
+    path: 'hello_world/index.html',
+    pathToServe: 'example');
+
+ChromeProxyService get service =>
+    fetchChromeProxyService(context.debugConnection);
+WipConnection get tabConnection => context.tabConnection;
+
+void main() {
+  group('shared context with evaluation', () {
+    setUpAll(() async {
+      await context.setUp();
+    });
+
+    tearDownAll(() async {
+      await context.tearDown();
+    });
+
+    group('breakpoint', () {
+      VM vm;
+      Isolate isolate;
+      ScriptList scripts;
+      ScriptRef mainScript;
+      Stream<Event> stream;
+
+      setUp(() async {
+        vm = await service.getVM();
+        isolate = await service.getIsolate(vm.isolates.first.id);
+        scripts = await service.getScripts(isolate.id);
+
+        await service.streamListen('Debug');
+        stream = service.onEvent('Debug');
+
+        mainScript = scripts.scripts
+            .firstWhere((each) => each.uri.contains('main.dart'));
+      });
+
+      tearDown(() async {
+        await service.resume(isolate.id);
+      });
+
+      test('set breakpoint', () async {
+        var line = await context.findBreakpointLine(
+            'printLocal', isolate.id, mainScript);
+        var bp = await service.addBreakpointWithScriptUri(
+            isolate.id, mainScript.uri, line);
+
+        await stream.firstWhere(
+            (Event event) => event.kind == EventKind.kPauseBreakpoint);
+
+        expect(bp, isNotNull);
+
+        // Remove breakpoint so it doesn't impact other tests.
+        await service.removeBreakpoint(isolate.id, bp.id);
+      });
+
+      test('set breakpoint again', () async {
+        var line = await context.findBreakpointLine(
+            'printLocal', isolate.id, mainScript);
+        var bp = await service.addBreakpointWithScriptUri(
+            isolate.id, mainScript.uri, line);
+
+        await stream.firstWhere(
+            (Event event) => event.kind == EventKind.kPauseBreakpoint);
+
+        expect(bp, isNotNull);
+
+        // Remove breakpoint so it doesn't impact other tests.
+        await service.removeBreakpoint(isolate.id, bp.id);
+      });
+    });
+  });
+}
diff --git a/dwds/test/chrome_proxy_service_test.dart b/dwds/test/chrome_proxy_service_test.dart
index dbed7e6..31a8cd7 100644
--- a/dwds/test/chrome_proxy_service_test.dart
+++ b/dwds/test/chrome_proxy_service_test.dart
@@ -19,7 +19,6 @@
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
 import 'fixtures/context.dart';
-import 'fixtures/fakes.dart';
 
 final context = TestContext();
 ChromeProxyService get service =>
@@ -1153,17 +1152,9 @@
     });
   });
 
-  group('shared context with evaluation', () {
+  group('shared context with fake evaluation', () {
     setUpAll(() async {
-      await context.setUp(
-          // TODO(annagrin): use actual compilation via frontend when supported
-          //
-          // This group of tests currently omits actual compilation
-          // from dart to js, so expressions given to evaluateInFrame
-          // in all tests below are written in javascript.
-          // Note frontend server has compilation tests, so the tests
-          // below make sure that the rest of the evaluation logic works.
-          expressionCompiler: FakeExpressionCompiler());
+      await context.setUp(useFakeExpressionCompiler: true);
     });
 
     tearDownAll(() async {
diff --git a/dwds/test/debugging/modules_test.dart b/dwds/test/debugging/modules_test.dart
index 1d71b15..f75d631 100644
--- a/dwds/test/debugging/modules_test.dart
+++ b/dwds/test/debugging/modules_test.dart
@@ -3,40 +3,46 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:dwds/src/debugging/modules.dart';
+
 import 'package:test/test.dart';
 
+import '../fixtures/fakes.dart';
+
 void main() {
   group('noteModule', () {
+    var webkitDebugger = FakeWebkitDebugger();
+    var executionContext = FakeExecutionContext();
+
     test('handles Google3 URLs', () async {
-      var modules = Modules(null, '', null);
+      var modules = Modules(webkitDebugger, '', executionContext)..initialize();
       await modules.noteModule('foo/google3/bar/blah.ddc.js', '10');
       expect(await modules.scriptIdForModule('bar/blah'), equals('10'));
-    });
+    }, skip: '[issue #917](https://github.com/dart-lang/webdev/issues/917)');
 
     test('ignores non-module paths', () async {
-      var modules = Modules(null, '', null);
+      var modules = Modules(webkitDebugger, '', executionContext)..initialize();
       await modules.noteModule('foo/bar', '10');
       expect(await modules.scriptIdForModule('foo/bar'), isNull);
     });
 
     test('rewrites third_party Google3 paths', () async {
-      var modules = Modules(null, '', null);
+      var modules = Modules(webkitDebugger, '', executionContext)..initialize();
       await modules.noteModule('/third_party/dart/test/lib/test.ddc.js', '10');
       expect(
           await modules.scriptIdForModule('packages/test/test'), equals('10'));
-    });
+    }, skip: '[issue #917](https://github.com/dart-lang/webdev/issues/917)');
 
     test('handles package paths', () async {
-      var modules = Modules(null, '', null);
+      var modules = Modules(webkitDebugger, '', executionContext)..initialize();
       await modules.noteModule('/packages/shelf/shelf.ddc.js', '10');
       expect(await modules.scriptIdForModule('packages/shelf/shelf'),
           equals('10'));
     });
 
     test('handles absolute paths', () async {
-      var modules = Modules(null, '', null);
+      var modules = Modules(webkitDebugger, '', executionContext)..initialize();
       await modules.noteModule('/foo/bar.ddc.js', '10');
       expect(await modules.scriptIdForModule('foo/bar'), equals('10'));
-    });
+    }, skip: '[issue #917](https://github.com/dart-lang/webdev/issues/917)');
   });
 }
diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart
index 31239f4..01c0f13 100644
--- a/dwds/test/fixtures/context.dart
+++ b/dwds/test/fixtures/context.dart
@@ -14,31 +14,42 @@
 import 'package:dwds/src/services/expression_compiler.dart';
 import 'package:dwds/src/utilities/dart_uri.dart';
 import 'package:dwds/src/utilities/shared.dart';
+import 'package:dwds/src/loaders/frontend_server_require.dart';
+import 'package:frontend_server_common/src/resident_runner.dart';
+import 'package:logging/logging.dart';
 import 'package:path/path.dart' as p;
+import 'package:shelf/shelf.dart';
+import 'package:shelf_proxy/shelf_proxy.dart';
 import 'package:test/test.dart';
 import 'package:vm_service/vm_service.dart';
 import 'package:webdriver/io.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
+import 'fakes.dart';
 import 'server.dart';
 import 'utilities.dart';
 
 final _batExt = Platform.isWindows ? '.bat' : '';
 final _exeExt = Platform.isWindows ? '.exe' : '';
 
+enum CompilationMode { buildDaemon, frontendServer }
+
 class TestContext {
   String appUrl;
   WipConnection tabConnection;
   WipConnection extensionConnection;
   TestServer testServer;
   BuildDaemonClient daemonClient;
+  ResidentWebRunner webRunner;
   WebDriver webDriver;
   Process chromeDriver;
   AppConnection appConnection;
   DebugConnection debugConnection;
   WebkitDebugger webkitDebugger;
   int port;
+  Directory _outputDir;
   File _entryFile;
+  String _packagesFilePath;
   String _entryContents;
 
   /// Top level directory in which we run the test server..
@@ -52,16 +63,23 @@
 
   TestContext(
       {String directory,
+      String entry,
       this.path = 'hello_world/index.html',
       this.pathToServe = 'example'}) {
-    workingDirectory = p.normalize(p.absolute(
-        directory ?? p.relative('../fixtures/_test', from: p.current)));
+    var relativeDirectory = p.join('..', 'fixtures', '_test');
+
+    var relativeEntry = p.join(
+        '..', 'fixtures', '_test', 'example', 'append_body', 'main.dart');
+
+    workingDirectory = p.normalize(p
+        .absolute(directory ?? p.relative(relativeDirectory, from: p.current)));
+
     DartUri.currentDirectory = workingDirectory;
-    _entryFile = File(p.absolute(p.join(
-        p.relative('../fixtures/_test', from: p.current),
-        'example',
-        'append_body',
-        'main.dart')));
+    _packagesFilePath = p.join(workingDirectory, '.packages');
+
+    _entryFile = File(p.normalize(
+        p.absolute(entry ?? p.relative(relativeEntry, from: p.current))));
+
     _entryContents = _entryFile.readAsStringSync();
   }
 
@@ -75,20 +93,21 @@
       bool waitToDebug,
       UrlEncoder urlEncoder,
       bool restoreBreakpoints,
-      bool useBuildDaemon,
-      ExpressionCompiler expressionCompiler}) async {
+      CompilationMode compilationMode,
+      bool useFakeExpressionCompiler,
+      LogWriter logWriter}) async {
     reloadConfiguration ??= ReloadConfiguration.none;
     serveDevTools ??= false;
     enableDebugExtension ??= false;
     autoRun ??= true;
     enableDebugging ??= true;
     waitToDebug ??= false;
-    useBuildDaemon ??= true;
+    compilationMode ??= CompilationMode.buildDaemon;
+    useFakeExpressionCompiler ??= false;
+    logWriter ??= (Level level, String message) => printOnFailure(message);
 
-    // TODO(grouma) - Support testing with the Frontend Server.
-    if (!useBuildDaemon) {
-      throw StateError('Only Build Daemon is supported with testing.');
-    }
+    var systemTempDir = Directory.systemTemp;
+    _outputDir = systemTempDir.createTempSync('foo bar');
 
     var chromeDriverPort = await findUnusedPort();
     var chromeDriverUrlBase = 'wd/hub';
@@ -109,16 +128,73 @@
     await Process.run('pub$_batExt', ['upgrade'],
         workingDirectory: workingDirectory);
 
-    daemonClient = await connectClient(
-        workingDirectory, [], (log) => printOnFailure(log.toString()));
-    daemonClient.registerBuildTarget(
-        DefaultBuildTarget((b) => b..target = pathToServe));
-    daemonClient.startBuild();
+    ExpressionCompiler expressionCompiler;
+    AssetReader assetReader;
+    Handler assetHandler;
+    Stream<BuildResults> buildResults;
+    RequireStrategy requireStrategy;
 
-    await daemonClient.buildResults
-        .firstWhere((results) => results.results
-            .any((result) => result.status == BuildStatus.succeeded))
-        .timeout(const Duration(seconds: 60));
+    switch (compilationMode) {
+      case CompilationMode.buildDaemon:
+        {
+          daemonClient = await connectClient(
+              workingDirectory, [], (log) => printOnFailure(log.toString()));
+          daemonClient.registerBuildTarget(
+              DefaultBuildTarget((b) => b..target = pathToServe));
+          daemonClient.startBuild();
+
+          await daemonClient.buildResults
+              .firstWhere((results) => results.results
+                  .any((result) => result.status == BuildStatus.succeeded))
+              .timeout(const Duration(seconds: 60));
+
+          var assetServerPort = daemonPort(workingDirectory);
+          assetHandler =
+              proxyHandler('http://localhost:$assetServerPort/$pathToServe/');
+          assetReader = ProxyServerAssetReader(assetServerPort, logWriter,
+              root: pathToServe);
+          requireStrategy = BuildRunnerRequireStrategyProvider(
+                  assetHandler, reloadConfiguration)
+              .strategy;
+
+          buildResults = daemonClient.buildResults;
+        }
+        break;
+      case CompilationMode.frontendServer:
+        {
+          var fileSystemRoot = p.dirname(_packagesFilePath);
+          var entryPath = _entryFile.path.substring(fileSystemRoot.length + 1);
+          webRunner = ResidentWebRunner(
+              entryPath,
+              urlEncoder,
+              fileSystemRoot,
+              _packagesFilePath,
+              [fileSystemRoot],
+              'org-dartlang-app',
+              _outputDir.path,
+              logWriter);
+
+          var assetServerPort = await findUnusedPort();
+          await webRunner.run(hostname, assetServerPort, pathToServe);
+
+          expressionCompiler = webRunner.expressionCompiler;
+          assetReader = webRunner.devFS.assetServer;
+          assetHandler = webRunner.devFS.assetServer.handleRequest;
+
+          requireStrategy = FrontendServerRequireStrategyProvider(
+                  webRunner.modules, reloadConfiguration)
+              .strategy;
+
+          buildResults = const Stream<BuildResults>.empty();
+        }
+        break;
+      default:
+        throw Exception('Unsupported compilation mode: $compilationMode');
+    }
+
+    expressionCompiler = useFakeExpressionCompiler
+        ? FakeExpressionCompiler()
+        : expressionCompiler;
 
     var debugPort = await findUnusedPort();
     // If the environment variable DWDS_DEBUG_CHROME is set to the string true
@@ -148,18 +224,20 @@
     testServer = await TestServer.start(
         hostname,
         port,
-        daemonPort(workingDirectory),
+        assetHandler,
+        assetReader,
+        requireStrategy,
         pathToServe,
-        daemonClient.buildResults,
+        buildResults,
         () async => connection,
-        reloadConfiguration,
         serveDevTools,
         enableDebugExtension,
         autoRun,
         enableDebugging,
         urlEncoder,
         restoreBreakpoints,
-        expressionCompiler);
+        expressionCompiler,
+        logWriter);
 
     appUrl = 'http://localhost:$port/$path';
     await webDriver.get(appUrl);
@@ -191,7 +269,9 @@
     DartUri.currentDirectory = p.current;
     _entryFile.writeAsStringSync(_entryContents);
     await daemonClient?.close();
+    await webRunner?.stop();
     await testServer?.stop();
+    await _outputDir?.delete(recursive: true);
   }
 
   Future<void> changeInput() async {
diff --git a/dwds/test/fixtures/fakes.dart b/dwds/test/fixtures/fakes.dart
index e23fc3c..b97ff76 100644
--- a/dwds/test/fixtures/fakes.dart
+++ b/dwds/test/fixtures/fakes.dart
@@ -6,6 +6,7 @@
 
 import 'package:async/src/stream_sink_transformer.dart';
 import 'package:dwds/dwds.dart';
+import 'package:dwds/src/debugging/execution_context.dart';
 import 'package:dwds/src/debugging/inspector.dart';
 import 'package:dwds/src/debugging/instance.dart';
 import 'package:dwds/src/debugging/webkit_debugger.dart';
@@ -182,6 +183,30 @@
         'result': {'result': <String, dynamic>{}}
       });
     }
+    if (method == 'Runtime.evaluate') {
+      // Fake response adapted from modules query at google3
+      return WipResponse({
+        'id': 42,
+        'result': {
+          'result': <String, dynamic>{
+            'type': 'object',
+            'value': <String, dynamic>{
+              // dart source Uri : js module name
+              'dart:io': 'dart_sdk',
+              'google3:///dart/tools/iblaze/web/hello_world.dart':
+                  'dart/tools/iblaze/web/hello_world_angular_library',
+              'package:ads.acx2.rpc.proto_mixin/ess_proto_mixin.dart':
+                  'ads/acx2/rpc/proto_mixin/lib/proto_mixin',
+              'package:collection/collection.dart: ':
+                  'third_party/dart/collection/lib/collection',
+              'package:collection/src/algorithms.dart':
+                  'third_party/dart/collection/lib/collection',
+              'package:shelf/shelf.dart': 'packages/shelf/shelf',
+            }
+          }
+        }
+      });
+    }
     return null;
   }
 
@@ -216,6 +241,16 @@
   Future<void> enablePage() => null;
 }
 
+/// Fake execution context that is needed for id only
+class FakeExecutionContext extends ExecutionContext {
+  @override
+  Future<int> get id async {
+    return 0;
+  }
+
+  FakeExecutionContext();
+}
+
 /// Fake expression compiler that simply passes expression through,
 /// without actual compilation
 class FakeExpressionCompiler implements ExpressionCompiler {
diff --git a/dwds/test/fixtures/server.dart b/dwds/test/fixtures/server.dart
index a3bf5ca..324db3c 100644
--- a/dwds/test/fixtures/server.dart
+++ b/dwds/test/fixtures/server.dart
@@ -8,14 +8,22 @@
 import 'package:dwds/data/build_result.dart';
 import 'package:dwds/dwds.dart';
 import 'package:dwds/src/services/expression_compiler.dart';
+import 'package:dwds/src/utilities/shared.dart';
 import 'package:http_multi_server/http_multi_server.dart';
-import 'package:logging/logging.dart';
 import 'package:shelf/shelf.dart';
 import 'package:shelf/shelf_io.dart' as shelf_io;
-import 'package:shelf_proxy/shelf_proxy.dart';
-import 'package:test/test.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
+Handler _interceptFavicon(Handler handler) {
+  return (request) async {
+    if (request.url.pathSegments.isNotEmpty &&
+        request.url.pathSegments.last == 'favicon.ico') {
+      return Response.ok('');
+    }
+    return handler(request);
+  };
+}
+
 class TestServer {
   final HttpServer _server;
   final String target;
@@ -49,20 +57,24 @@
   static Future<TestServer> start(
       String hostname,
       int port,
-      int assetServerPort,
+      Handler assetHandler,
+      AssetReader assetReader,
+      RequireStrategy strategy,
       String target,
       Stream<daemon.BuildResults> buildResults,
       Future<ChromeConnection> Function() chromeConnection,
-      ReloadConfiguration reloadConfiguration,
       bool serveDevTools,
       bool enableDebugExtension,
       bool autoRun,
       bool enableDebugging,
       UrlEncoder urlEncoder,
       bool restoreBreakpoints,
-      ExpressionCompiler expressionCompiler) async {
+      ExpressionCompiler expressionCompiler,
+      LogWriter logWriter) async {
     var pipeline = const Pipeline();
 
+    pipeline = pipeline.addMiddleware(_interceptFavicon);
+
     var filteredBuildResults = buildResults.asyncMap<BuildResult>((results) {
       var result =
           results.results.firstWhere((result) => result.target == target);
@@ -77,22 +89,12 @@
       throw StateError('Unexpected Daemon build result: $result');
     });
 
-    var logWriter = (Level level, String message) => printOnFailure(message);
-
-    var assetReader =
-        ProxyServerAssetReader(assetServerPort, logWriter, root: target);
-
-    var assetHandler =
-        proxyHandler('http://localhost:$assetServerPort/$target/');
-
     var dwds = await Dwds.start(
       assetReader: assetReader,
       buildResults: filteredBuildResults,
       chromeConnection: chromeConnection,
       logWriter: logWriter,
-      loadStrategy:
-          BuildRunnerRequireStrategyProvider(assetHandler, reloadConfiguration)
-              .strategy,
+      loadStrategy: strategy,
       serveDevTools: serveDevTools,
       enableDebugExtension: enableDebugExtension,
       enableDebugging: enableDebugging,
diff --git a/dwds/test/fixtures/utilities.dart b/dwds/test/fixtures/utilities.dart
index dd2f32a..904056e 100644
--- a/dwds/test/fixtures/utilities.dart
+++ b/dwds/test/fixtures/utilities.dart
@@ -32,6 +32,7 @@
   return aboveExecutable;
 })();
 
+final String dartSdkPath = _sdkDir;
 final String dartPath = p.join(_sdkDir, 'bin', 'dart');
 final String pubSnapshot =
     p.join(_sdkDir, 'bin', 'snapshots', 'pub.dart.snapshot');
diff --git a/dwds/test/frontend_server_breakpoint_test.dart b/dwds/test/frontend_server_breakpoint_test.dart
new file mode 100644
index 0000000..406377a
--- /dev/null
+++ b/dwds/test/frontend_server_breakpoint_test.dart
@@ -0,0 +1,91 @@
+// 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.
+
+@Tags(['frontend-server'])
+@TestOn('vm')
+import 'dart:async';
+
+import 'package:dwds/src/connections/debug_connection.dart';
+import 'package:dwds/src/services/chrome_proxy_service.dart';
+import 'package:test/test.dart';
+import 'package:vm_service/vm_service.dart';
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+
+import 'fixtures/context.dart';
+
+final context = TestContext(
+    directory: '../fixtures/_testPackage',
+    entry: '../fixtures/_testPackage/web/main.dart',
+    path: 'index.html',
+    pathToServe: 'web');
+
+ChromeProxyService get service =>
+    fetchChromeProxyService(context.debugConnection);
+WipConnection get tabConnection => context.tabConnection;
+
+void main() {
+  group('shared context with evaluation', () {
+    setUpAll(() async {
+      await context.setUp(compilationMode: CompilationMode.frontendServer);
+    });
+
+    tearDownAll(() async {
+      await context.tearDown();
+    });
+
+    group('breakpoint', () {
+      VM vm;
+      Isolate isolate;
+      ScriptList scripts;
+      ScriptRef mainScript;
+      Stream<Event> stream;
+
+      setUp(() async {
+        vm = await service.getVM();
+        isolate = await service.getIsolate(vm.isolates.first.id);
+        scripts = await service.getScripts(isolate.id);
+
+        await service.streamListen('Debug');
+        stream = service.onEvent('Debug');
+
+        mainScript = scripts.scripts
+            .firstWhere((each) => each.uri.contains('main.dart'));
+      });
+
+      tearDown(() async {
+        await service.resume(isolate.id);
+      });
+
+      test('set breakpoint', () async {
+        var line = await context.findBreakpointLine(
+            'printLocal', isolate.id, mainScript);
+        var bp = await service.addBreakpointWithScriptUri(
+            isolate.id, mainScript.uri, line);
+
+        await stream.firstWhere(
+            (Event event) => event.kind == EventKind.kPauseBreakpoint);
+
+        expect(bp, isNotNull);
+
+        // Remove breakpoint so it doesn't impact other tests.
+        await service.removeBreakpoint(isolate.id, bp.id);
+      });
+
+      test('set breakpoint again', () async {
+        var line = await context.findBreakpointLine(
+            'printLocal', isolate.id, mainScript);
+        var bp = await service.addBreakpointWithScriptUri(
+            isolate.id, mainScript.uri, line);
+
+        await stream.firstWhere(
+            (Event event) => event.kind == EventKind.kPauseBreakpoint);
+
+        expect(bp, isNotNull);
+
+        // Remove breakpoint so it doesn't impact other tests.
+        await service.removeBreakpoint(isolate.id, bp.id);
+      });
+    });
+  });
+}
diff --git a/dwds/test/frontend_server_evaluate_test.dart b/dwds/test/frontend_server_evaluate_test.dart
new file mode 100644
index 0000000..5020056
--- /dev/null
+++ b/dwds/test/frontend_server_evaluate_test.dart
@@ -0,0 +1,168 @@
+// 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.
+
+@Tags(['frontend-server'])
+@TestOn('vm')
+import 'dart:async';
+
+import 'package:dwds/src/connections/debug_connection.dart';
+import 'package:dwds/src/services/chrome_proxy_service.dart';
+import 'package:test/test.dart';
+import 'package:vm_service/vm_service.dart';
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+
+import 'fixtures/context.dart';
+
+final context = TestContext(
+    directory: '../fixtures/_testPackage',
+    entry: '../fixtures/_testPackage/web/main.dart',
+    path: 'index.html',
+    pathToServe: 'web');
+
+ChromeProxyService get service =>
+    fetchChromeProxyService(context.debugConnection);
+WipConnection get tabConnection => context.tabConnection;
+
+void main() {
+  group('shared context with evaluation', () {
+    setUpAll(() async {
+      await context.setUp(compilationMode: CompilationMode.frontendServer);
+    });
+
+    tearDownAll(() async {
+      await context.tearDown();
+    });
+
+    group('evaluateInFrame', () {
+      VM vm;
+      Isolate isolate;
+      ScriptList scripts;
+      ScriptRef mainScript;
+      Stream<Event> stream;
+
+      setUp(() async {
+        vm = await service.getVM();
+        isolate = await service.getIsolate(vm.isolates.first.id);
+        scripts = await service.getScripts(isolate.id);
+
+        await service.streamListen('Debug');
+        stream = service.onEvent('Debug');
+
+        mainScript = scripts.scripts
+            .firstWhere((each) => each.uri.contains('main.dart'));
+      });
+
+      tearDown(() async {
+        await service.resume(isolate.id);
+      });
+
+      test('local', () async {
+        var line = await context.findBreakpointLine(
+            'printLocal', isolate.id, mainScript);
+        var bp = await service.addBreakpointWithScriptUri(
+            isolate.id, mainScript.uri, line);
+
+        var event = await stream.firstWhere(
+            (Event event) => event.kind == EventKind.kPauseBreakpoint);
+
+        var result = await service.evaluateInFrame(
+            isolate.id, event.topFrame.index, 'local');
+
+        expect(
+            result,
+            const TypeMatcher<InstanceRef>().having(
+                (instance) => instance.valueAsString, 'valueAsString', '42'));
+
+        // Remove breakpoint so it doesn't impact other tests.
+        await service.removeBreakpoint(isolate.id, bp.id);
+      });
+
+      test('global', () async {
+        var line = await context.findBreakpointLine(
+            'printGlobal', isolate.id, mainScript);
+        var bp = await service.addBreakpointWithScriptUri(
+            isolate.id, mainScript.uri, line);
+
+        var event = await stream.firstWhere(
+            (Event event) => event.kind == EventKind.kPauseBreakpoint);
+
+        var result = await service.evaluateInFrame(
+            isolate.id, event.topFrame.index, 'valueFromTestPackage');
+
+        expect(
+            result,
+            const TypeMatcher<InstanceRef>().having(
+                (instance) => instance.valueAsString, 'valueAsString', '3'));
+
+        // Remove breakpoint so it doesn't impact other tests.
+        await service.removeBreakpoint(isolate.id, bp.id);
+      });
+
+      test('call core function', () async {
+        var line = await context.findBreakpointLine(
+            'printLocal', isolate.id, mainScript);
+        var bp = await service.addBreakpointWithScriptUri(
+            isolate.id, mainScript.uri, line);
+
+        var event = await stream.firstWhere(
+            (Event event) => event.kind == EventKind.kPauseBreakpoint);
+
+        var result = await service.evaluateInFrame(
+            isolate.id, event.topFrame.index, 'print(local)');
+
+        expect(
+            result,
+            const TypeMatcher<InstanceRef>().having(
+                (instance) => instance.valueAsString, 'valueAsString', 'null'));
+
+        // Remove breakpoint so it doesn't impact other tests.
+        await service.removeBreakpoint(isolate.id, bp.id);
+      });
+
+      test('call library function', () async {
+        var line = await context.findBreakpointLine(
+            'printLocal', isolate.id, mainScript);
+        var bp = await service.addBreakpointWithScriptUri(
+            isolate.id, mainScript.uri, line);
+
+        var event = await stream.firstWhere(
+            (Event event) => event.kind == EventKind.kPauseBreakpoint);
+
+        var result = await service.evaluateInFrame(
+            isolate.id, event.topFrame.index, 'testLibraryFunction(local)');
+
+        expect(
+            result,
+            const TypeMatcher<InstanceRef>().having(
+                (instance) => instance.valueAsString, 'valueAsString', '42'));
+
+        // Remove breakpoint so it doesn't impact other tests.
+        await service.removeBreakpoint(isolate.id, bp.id);
+      });
+
+      test('error', () async {
+        var line = await context.findBreakpointLine(
+            'printLocal', isolate.id, mainScript);
+        var bp = await service.addBreakpointWithScriptUri(
+            isolate.id, mainScript.uri, line);
+
+        var event = await stream.firstWhere(
+            (Event event) => event.kind == EventKind.kPauseBreakpoint);
+
+        var error = await service.evaluateInFrame(
+            isolate.id, event.topFrame.index, 'typo');
+
+        expect(
+            error,
+            const TypeMatcher<InstanceRef>().having(
+                (instance) => instance.valueAsString,
+                'valueAsString',
+                'Compilation error: Getter not found: \'typo\'.\ntypo\n^^^^'));
+
+        // Remove breakpoint so it doesn't impact other tests.
+        await service.removeBreakpoint(isolate.id, bp.id);
+      });
+    });
+  });
+}
diff --git a/dwds/test/readers/proxy_server_asset_reader_test.dart b/dwds/test/readers/proxy_server_asset_reader_test.dart
index 5365d16..0bae993 100644
--- a/dwds/test/readers/proxy_server_asset_reader_test.dart
+++ b/dwds/test/readers/proxy_server_asset_reader_test.dart
@@ -11,7 +11,7 @@
   final context = TestContext();
   ProxyServerAssetReader assetReader;
   setUpAll(() async {
-    await context.setUp(useBuildDaemon: true);
+    await context.setUp();
     assetReader = context.testServer.assetReader as ProxyServerAssetReader;
   });
 
diff --git a/fixtures/_testPackage/lib/test_library.dart b/fixtures/_testPackage/lib/test_library.dart
index 2eecb43..e9a850d 100644
--- a/fixtures/_testPackage/lib/test_library.dart
+++ b/fixtures/_testPackage/lib/test_library.dart
@@ -3,3 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 int valueFromTestPackage = 3;
+
+int testLibraryFunction(int formal) {
+  return formal;
+}
diff --git a/fixtures/_testPackage/web/main.dart b/fixtures/_testPackage/web/main.dart
index 57173c9..5c1537d 100644
--- a/fixtures/_testPackage/web/main.dart
+++ b/fixtures/_testPackage/web/main.dart
@@ -15,5 +15,16 @@
     print(valueFromTestPackage);
   });
 
+  // for evaluation
+  Timer.periodic(const Duration(seconds: 1), (_) {
+    printSomething();
+  });
+
   document.body.appendText(concatenate('Program', ' is running!'));
 }
+
+void printSomething() {
+  var local = 42;
+  print('Local is: $local'); // Breakpoint: printLocal
+  print(valueFromTestPackage); // Breakpoint: printGlobal
+}
diff --git a/frontend_server_common/CHANGELOG.md b/frontend_server_common/CHANGELOG.md
new file mode 100644
index 0000000..51d1510
--- /dev/null
+++ b/frontend_server_common/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.1.0
+
+- Initial version
diff --git a/frontend_server_common/LICENSE b/frontend_server_common/LICENSE
new file mode 100644
index 0000000..18daf2b
--- /dev/null
+++ b/frontend_server_common/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2020, the Dart project authors. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/frontend_server_common/README.md b/frontend_server_common/README.md
new file mode 100644
index 0000000..ab54d27
--- /dev/null
+++ b/frontend_server_common/README.md
@@ -0,0 +1,14 @@
+Dart Web Developer Service
+
+__*Note: Under heavy development.*__
+
+This code is an edited copy of flutter code used for setting up frontend server
+and components that are needed to communicate to Chrome and dwds:
+
+- frontend server client
+- web runner
+- dev fs
+- asset server
+
+This eventually will transform into common code that both flutter and dwds use
+for better integration.
\ No newline at end of file
diff --git a/frontend_server_common/lib/src/asset.dart b/frontend_server_common/lib/src/asset.dart
new file mode 100644
index 0000000..28d8faf
--- /dev/null
+++ b/frontend_server_common/lib/src/asset.dart
@@ -0,0 +1,62 @@
+// Copyright 2020 The Dart Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Note: this is a copy from flutter tools, updated to work with dwds tests,
+// most functionality removed
+
+import 'dart:async';
+
+import 'devfs_content.dart';
+
+const AssetBundleFactory _manifestFactory = _MockManifestAssetBundleFactory();
+
+const String defaultManifestPath = 'pubspec.yaml';
+
+/// Injected factory class for spawning [AssetBundle] instances.
+abstract class AssetBundleFactory {
+  static AssetBundleFactory get defaultInstance => _manifestFactory;
+
+  /// Creates a new [AssetBundle].
+  AssetBundle createBundle();
+}
+
+abstract class AssetBundle {
+  Map<String, DevFSContent> get entries;
+
+  /// Returns 0 for success; non-zero for failure.
+  Future<int> build({
+    String manifestPath = defaultManifestPath,
+    String assetDirPath,
+    String packagesPath,
+    bool includeDefaultFonts = true,
+    bool reportLicensedPackages = false,
+  });
+}
+
+class _MockManifestAssetBundleFactory implements AssetBundleFactory {
+  const _MockManifestAssetBundleFactory();
+
+  @override
+  AssetBundle createBundle() => _MockManifestAssetBundle();
+}
+
+class _MockManifestAssetBundle implements AssetBundle {
+  /// Constructs an [_MockManifestAssetBundle] that gathers the set of assets from the
+  /// pubspec.yaml manifest.
+  _MockManifestAssetBundle();
+
+  @override
+  final Map<String, DevFSContent> entries = <String, DevFSContent>{};
+
+  @override
+  Future<int> build({
+    String manifestPath = defaultManifestPath,
+    String assetDirPath,
+    String packagesPath,
+    bool includeDefaultFonts = true,
+    bool reportLicensedPackages = false,
+  }) async {
+    return 0;
+  }
+}
diff --git a/frontend_server_common/lib/src/asset_server.dart b/frontend_server_common/lib/src/asset_server.dart
new file mode 100644
index 0000000..5fab1cc
--- /dev/null
+++ b/frontend_server_common/lib/src/asset_server.dart
@@ -0,0 +1,281 @@
+// Copyright 2020 The Dart Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Note: this is a copy from flutter tools, updated to work with dwds tests
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:dwds/dwds.dart';
+import 'package:file/file.dart';
+import 'package:mime/mime.dart' as mime;
+// ignore: deprecated_member_use
+import 'package:package_config/discovery.dart';
+// ignore: deprecated_member_use
+import 'package:package_config/packages.dart';
+import 'package:path/path.dart' as p;
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf/shelf_io.dart' as shelf;
+import 'package:logging/logging.dart';
+
+import 'utilities.dart';
+
+class TestAssetServer implements AssetReader {
+  TestAssetServer(this._root, this._httpServer, this._packages,
+      this.internetAddress, this._fileSystem, this._logWriter);
+
+  // Fallback to "application/octet-stream" on null which
+  // makes no claims as to the structure of the data.
+  static const String _defaultMimeType = 'application/octet-stream';
+  final FileSystem _fileSystem;
+
+  void _printTrace(String message) {
+    _logWriter(Level.INFO, message);
+  }
+
+  /// Start the web asset server on a [hostname] and [port].
+  ///
+  /// Unhandled exceptions will throw a exception with the error and stack
+  /// trace.
+  static Future<TestAssetServer> start(
+      FileSystem fileSystem,
+      String root,
+      String hostname,
+      int port,
+      UrlEncoder urlTunneller,
+      LogWriter logWriter) async {
+    var address = (await InternetAddress.lookup(hostname)).first;
+    var httpServer = await HttpServer.bind(address, port);
+    var packages = await loadPackagesFile(Uri.base.resolve('.packages'),
+        loader: (Uri uri) => fileSystem.file(uri).readAsBytes());
+    var server = TestAssetServer(
+        root, httpServer, packages, address, fileSystem, logWriter);
+
+    return server;
+  }
+
+  final String _root;
+  final HttpServer _httpServer;
+  // If holding these in memory is too much overhead, this can be switched to a
+  // RandomAccessFile and read on demand.
+  final Map<String, Uint8List> _files = <String, Uint8List>{};
+  final Map<String, Uint8List> _sourcemaps = <String, Uint8List>{};
+  // ignore: deprecated_member_use
+  final Packages _packages;
+  final InternetAddress internetAddress;
+  final LogWriter _logWriter;
+
+  Uint8List getFile(String path) => _files[path];
+
+  Uint8List getSourceMap(String path) => _sourcemaps[path];
+
+  // handle requests for JavaScript source, dart sources maps, or asset files.
+  Future<shelf.Response> handleRequest(shelf.Request request) async {
+    var headers = <String, String>{};
+
+    // Index file is serverd from the _root directory
+    if (request.url.path.endsWith('index.html')) {
+      final indexFile = _fileSystem.currentDirectory
+          .childDirectory(_root)
+          .childFile('index.html');
+      if (indexFile.existsSync()) {
+        headers[HttpHeaders.contentTypeHeader] = 'text/html';
+        headers[HttpHeaders.contentLengthHeader] =
+            indexFile.lengthSync().toString();
+        return shelf.Response.ok(indexFile.openRead(), headers: headers);
+      }
+      return shelf.Response.notFound('');
+    }
+
+    // NOTE: shelf removes leading `/` for some reason.
+    var requestPath = request.url.path.startsWith('/')
+        ? request.url.path
+        : '/${request.url.path}';
+
+    // If this is a JavaScript file, it must be in the in-memory cache.
+    // Attempt to look up the file by URI.
+    if (_files.containsKey(requestPath)) {
+      final List<int> bytes = getFile(requestPath);
+      headers[HttpHeaders.contentLengthHeader] = bytes.length.toString();
+      headers[HttpHeaders.contentTypeHeader] = 'application/javascript';
+      return shelf.Response.ok(bytes, headers: headers);
+    }
+    // If this is a sourcemap file, then it might be in the in-memory cache.
+    // Attempt to lookup the file by URI.
+    var sourceMapPath = _resolvePath(requestPath);
+    if (_sourcemaps.containsKey(sourceMapPath)) {
+      final List<int> bytes = getSourceMap(requestPath);
+      headers[HttpHeaders.contentLengthHeader] = bytes.length.toString();
+      headers[HttpHeaders.contentTypeHeader] = 'application/json';
+      return shelf.Response.ok(bytes, headers: headers);
+    }
+
+    var file = _resolveDartFile(requestPath);
+    if (!file.existsSync()) {
+      return shelf.Response.notFound('');
+    }
+
+    var length = file.lengthSync();
+    // Attempt to determine the file's mime type. if this is not provided some
+    // browsers will refuse to render images/show video et cetera. If the tool
+    // cannot determine a mime type, fall back to application/octet-stream.
+    String mimeType;
+    if (length >= 12) {
+      mimeType = mime.lookupMimeType(
+        file.path,
+        headerBytes: await file.openRead(0, 12).first,
+      );
+    }
+    mimeType ??= _defaultMimeType;
+    headers[HttpHeaders.contentLengthHeader] = length.toString();
+    headers[HttpHeaders.contentTypeHeader] = mimeType;
+    return shelf.Response.ok(file.openRead(), headers: headers);
+  }
+
+  /// Tear down the http server running.
+  Future<void> dispose() {
+    return _httpServer.close();
+  }
+
+  /// Write a single file into the in-memory cache.
+  void writeFile(String filePath, String contents) {
+    _files[filePath] = Uint8List.fromList(utf8.encode(contents));
+  }
+
+  /// Update the in-memory asset server with the provided source and manifest files.
+  ///
+  /// Returns a list of updated modules.
+  List<String> write(File codeFile, File manifestFile, File sourcemapFile) {
+    var modules = <String>[];
+    var codeBytes = codeFile.readAsBytesSync();
+    var sourcemapBytes = sourcemapFile.readAsBytesSync();
+    var manifest =
+        castStringKeyedMap(json.decode(manifestFile.readAsStringSync()));
+    for (var filePath in manifest.keys) {
+      if (filePath == null) {
+        _printTrace('Invalid manfiest file: $filePath');
+        continue;
+      }
+      var offsets = castStringKeyedMap(manifest[filePath]);
+      var codeOffsets = (offsets['code'] as List<dynamic>).cast<int>();
+      var sourcemapOffsets =
+          (offsets['sourcemap'] as List<dynamic>).cast<int>();
+      if (codeOffsets.length != 2 || sourcemapOffsets.length != 2) {
+        _printTrace('Invalid manifest byte offsets: $offsets');
+        continue;
+      }
+
+      var codeStart = codeOffsets[0];
+      var codeEnd = codeOffsets[1];
+      if (codeStart < 0 || codeEnd > codeBytes.lengthInBytes) {
+        _printTrace('Invalid byte index: [$codeStart, $codeEnd]');
+        continue;
+      }
+      var byteView = Uint8List.view(
+        codeBytes.buffer,
+        codeStart,
+        codeEnd - codeStart,
+      );
+      _files[filePath] = byteView;
+
+      var sourcemapStart = sourcemapOffsets[0];
+      var sourcemapEnd = sourcemapOffsets[1];
+      if (sourcemapStart < 0 || sourcemapEnd > sourcemapBytes.lengthInBytes) {
+        _printTrace('Invalid byte index: [$sourcemapStart, $sourcemapEnd]');
+        continue;
+      }
+      var sourcemapView = Uint8List.view(
+        sourcemapBytes.buffer,
+        sourcemapStart,
+        sourcemapEnd - sourcemapStart,
+      );
+      _sourcemaps['$filePath.map'] = sourcemapView;
+
+      modules.add(filePath);
+    }
+    return modules;
+  }
+
+  // Attempt to resolve `path` to a dart file.
+  File _resolveDartFile(String path) {
+    path = _resolvePath(path);
+
+    // If this is a dart file, it must be on the local file system and is
+    // likely coming from a source map request. The tool doesn't currently
+    // consider the case of Dart files as assets.
+    var dartFile =
+        _fileSystem.file(_fileSystem.currentDirectory.uri.resolve(path));
+    if (dartFile.existsSync()) {
+      return dartFile;
+    }
+
+    var segments = p.split(path);
+    if (segments.first.isEmpty) {
+      segments.removeAt(0);
+    }
+
+    // The file might have been a package file which is signaled by a
+    // `/packages/<package>/<path>` request.
+    if (segments.first == 'packages') {
+      var packageFile = _fileSystem.file(_packages
+          .resolve(Uri(scheme: 'package', pathSegments: segments.skip(1))));
+      if (packageFile.existsSync()) {
+        return packageFile;
+      }
+    }
+
+    // Otherwise it must be a Dart SDK source.
+    var dartSdkParent = _fileSystem.directory(dartSdkPath).parent;
+    var dartSdkFile = _fileSystem.file(
+        _fileSystem.path.joinAll(<String>[dartSdkParent.path, ...segments]));
+    return dartSdkFile;
+  }
+
+  // Mimick build_daemon by serving from the root (web)
+  // if the path does not belong to a package
+  // Note: This is a temporary workaround until we solve inconsistencies
+  // in different configurations by introducing module name and path
+  // translation interfaces between compiler, asset server, and the
+  // debugger.
+  // TODO(annagrin): module interface
+  // [issue #910](https://github.com/dart-lang/webdev/issues/910)
+  String _resolvePath(String path) {
+    var segments = p.split(path);
+    if (segments.first.isEmpty) {
+      segments.removeAt(0);
+    }
+
+    return path = segments.first == 'packages'
+        ? p.joinAll(segments)
+        : p.joinAll([_root, ...segments]);
+  }
+
+  @override
+  Future<String> dartSourceContents(String serverPath) {
+    var result = _resolveDartFile(serverPath);
+    if (result.existsSync()) {
+      return result.readAsString();
+    }
+    return null;
+  }
+
+  @override
+  Future<String> sourceMapContents(String serverPath) async {
+    var path = _resolvePath(serverPath);
+    path = '/$path';
+    if (_sourcemaps.containsKey(path)) {
+      return utf8.decode(_sourcemaps[path]);
+    }
+    return null;
+  }
+}
+
+/// Given a data structure which is a Map of String to dynamic values, return
+/// the same structure (`Map<String, dynamic>`) with the correct runtime types.
+Map<String, dynamic> castStringKeyedMap(dynamic untyped) {
+  var map = untyped as Map<dynamic, dynamic>;
+  return map?.cast<String, dynamic>();
+}
diff --git a/frontend_server_common/lib/src/bootstrap.dart b/frontend_server_common/lib/src/bootstrap.dart
new file mode 100644
index 0000000..ba2fc79
--- /dev/null
+++ b/frontend_server_common/lib/src/bootstrap.dart
@@ -0,0 +1,72 @@
+// Copyright 2020 The Dart Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Note: this is a copy from flutter tools, updated to work with dwds tests
+
+import 'package:meta/meta.dart';
+
+/// The JavaScript bootstrap script to support in-browser hot restart.
+///
+/// The [requireUrl] loads our cached RequireJS script file. The [mapperUrl]
+/// loads the special Dart stack trace mapper. The [entrypoint] is the
+/// actual main.dart file.
+///
+/// This file is served when the browser requests "main.dart.js" in debug mode,
+/// and is responsible for bootstrapping the RequireJS modules and attaching
+/// the hot reload hooks.
+String generateBootstrapScript({
+  @required String requireUrl,
+  @required String mapperUrl,
+  @required String entrypoint,
+}) {
+  return '''
+"use strict";
+
+// Attach source mapping.
+var mapperEl = document.createElement("script");
+mapperEl.defer = true;
+mapperEl.async = false;
+mapperEl.src = "$mapperUrl";
+document.head.appendChild(mapperEl);
+
+// Attach require JS.
+var requireEl = document.createElement("script");
+requireEl.defer = true;
+requireEl.async = false;
+requireEl.src = "$requireUrl";
+// This attribute tells require JS what to load as main (defined below).
+requireEl.setAttribute("data-main", "main_module.bootstrap");
+document.head.appendChild(requireEl);
+''';
+}
+
+/// Generate a synthetic main module which captures the application's main
+/// method.
+///
+/// RE: Object.keys usage in app.main:
+/// This attaches the main entrypoint and hot reload functionality to the window.
+/// The app module will have a single property which contains the actual application
+/// code. The property name is based off of the entrypoint that is generated, for example
+/// the file `foo/bar/baz.dart` will generate a property named approximately
+/// `foo__bar__baz`. Rather than attempt to guess, we assume the first property of
+/// this object is the module.
+String generateMainModule({@required String entrypoint}) {
+  return '''/* ENTRYPOINT_EXTENTION_MARKER */
+
+// Create the main module loaded below.
+define("main_module.bootstrap", ["$entrypoint", "dart_sdk"], function(app, dart_sdk) {
+  dart_sdk.dart.setStartAsyncSynchronously(true);
+  dart_sdk._isolate_helper.startRootIsolate(() => {}, []);
+  dart_sdk._debugger.registerDevtoolsFormatter();
+  let voidToNull = () => (voidToNull = dart_sdk.dart.constFn(dart_sdk.dart.fnType(dart_sdk.core.Null, [dart_sdk.dart.void])))();
+
+  // See the generateMainModule doc comment.
+  var child = {};
+  child.main = app[Object.keys(app)[0]].main;
+
+  /* MAIN_EXTENSION_MARKER */
+  child.main();
+});
+''';
+}
diff --git a/frontend_server_common/lib/src/devfs.dart b/frontend_server_common/lib/src/devfs.dart
new file mode 100644
index 0000000..707c72a
--- /dev/null
+++ b/frontend_server_common/lib/src/devfs.dart
@@ -0,0 +1,194 @@
+// Copyright 2020 The Dart Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Note: this is a copy from flutter tools, updated to work with dwds tests
+
+import 'dart:io';
+
+import 'package:dwds/dwds.dart';
+import 'package:file/file.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+
+import 'asset.dart';
+import 'asset_server.dart';
+import 'bootstrap.dart';
+import 'devfs_content.dart';
+import 'frontend_server_client.dart';
+import 'utilities.dart';
+
+final String dartWebSdkPath = p.join(dartSdkPath, 'lib', 'dev_compiler');
+
+class WebDevFS {
+  WebDevFS(
+      {this.fileSystem,
+      this.hostname,
+      this.port,
+      this.packagesFilePath,
+      this.packagesPath,
+      this.root,
+      this.urlTunneller,
+      this.logWriter});
+
+  final FileSystem fileSystem;
+  TestAssetServer assetServer;
+  final String hostname;
+  final int port;
+  final String packagesFilePath;
+  final String packagesPath;
+  final String root;
+  final UrlEncoder urlTunneller;
+  final LogWriter logWriter;
+  Directory _savedCurrentDirectory;
+  List<Uri> sources;
+
+  Future<Uri> create() async {
+    _savedCurrentDirectory = fileSystem.currentDirectory;
+    fileSystem.currentDirectory = packagesPath;
+    assetServer = await TestAssetServer.start(
+        fileSystem, root, hostname, port, urlTunneller, logWriter);
+    return Uri.parse('http://$hostname:$port');
+  }
+
+  Future<void> dispose() {
+    fileSystem.currentDirectory = _savedCurrentDirectory;
+    return assetServer.dispose();
+  }
+
+  Future<UpdateFSReport> update({
+    String mainPath,
+    AssetBundle bundle,
+    String dillOutputPath,
+    @required ResidentCompiler generator,
+    List<Uri> invalidatedFiles,
+  }) async {
+    assert(generator != null);
+    var outputDirectoryPath = fileSystem.file(mainPath).parent.path;
+    var entryPoint = mainPath;
+
+    assetServer.writeFile(
+      '/main.dart.js',
+      generateBootstrapScript(
+        requireUrl: _filePathToUriFragment(requireJS.path),
+        mapperUrl: _filePathToUriFragment(stackTraceMapper.path),
+        entrypoint: entryPoint,
+      ),
+    );
+    assetServer.writeFile(
+      '/main_module.bootstrap.js',
+      generateMainModule(
+        entrypoint: entryPoint,
+      ),
+    );
+
+    assetServer.writeFile('/main_module.digests', '{}');
+    assetServer.writeFile('/dart_sdk.js', dartSdk.readAsStringSync());
+    assetServer.writeFile(
+        '/dart_sdk.js.map', dartSdkSourcemap.readAsStringSync());
+    // TODO(jonahwilliams): refactor the asset code in this and the regular devfs to
+    // be shared.
+    if (bundle != null) {
+      await writeBundle(
+        fileSystem.directory(p.joinAll(['build', 'assets'])),
+        bundle.entries,
+      );
+    }
+
+    generator.reset();
+
+    var compilerOutput = await generator.recompile(
+      'org-dartlang-app:///$mainPath',
+      invalidatedFiles,
+      outputPath: p.join(dillOutputPath, 'app.dill'),
+      packagesFilePath: packagesFilePath,
+    );
+    if (compilerOutput == null || compilerOutput.errorCount > 0) {
+      return UpdateFSReport(success: false);
+    }
+
+    // list of sources that needs to be monitored are in [compilerOutput.sources]
+    sources = compilerOutput.sources;
+    File codeFile;
+    File manifestFile;
+    File sourcemapFile;
+    List<String> modules;
+    try {
+      var parentDirectory = fileSystem.directory(outputDirectoryPath);
+      codeFile =
+          parentDirectory.childFile('${compilerOutput.outputFilename}.sources');
+      manifestFile =
+          parentDirectory.childFile('${compilerOutput.outputFilename}.json');
+      sourcemapFile =
+          parentDirectory.childFile('${compilerOutput.outputFilename}.map');
+      modules = assetServer.write(codeFile, manifestFile, sourcemapFile);
+    } on FileSystemException catch (err) {
+      throw Exception('Failed to load recompiled sources:\n$err');
+    }
+    return UpdateFSReport(
+      success: true,
+      syncedBytes: codeFile.lengthSync(),
+      invalidatedSourcesCount: invalidatedFiles.length,
+    )..invalidatedModules = modules;
+  }
+
+  File get requireJS => fileSystem.file(fileSystem.path.join(
+        dartWebSdkPath,
+        'kernel',
+        'amd',
+        'require.js',
+      ));
+
+  File get dartSdk => fileSystem.file(fileSystem.path.join(
+        dartWebSdkPath,
+        'kernel',
+        'amd',
+        'dart_sdk.js',
+      ));
+
+  File get dartSdkSourcemap => fileSystem.file(fileSystem.path.join(
+        dartWebSdkPath,
+        'kernel',
+        'amd',
+        'dart_sdk.js.map',
+      ));
+
+  File get stackTraceMapper => fileSystem.file(fileSystem.path.join(
+        dartWebSdkPath,
+        'web',
+        'dart_stack_trace_mapper.js',
+      ));
+}
+
+String _filePathToUriFragment(String path) {
+  if (Platform.isWindows) {
+    var startWithSlash = path.startsWith('/');
+    var partial =
+        fileSystem.path.split(path).skip(startWithSlash ? 2 : 1).join('/');
+    if (partial.startsWith('/')) {
+      return partial;
+    }
+    return '/$partial';
+  }
+  return path;
+}
+
+Future<void> writeBundle(
+    Directory bundleDir, Map<String, DevFSContent> assetEntries) async {
+  if (bundleDir.existsSync()) {
+    try {
+      bundleDir.deleteSync(recursive: true);
+    } on FileSystemException catch (err) {
+      printError('Failed to clean up asset directory ${bundleDir.path}: $err\n'
+          'To clean build artifacts, use the command "flutter clean".');
+    }
+  }
+  bundleDir.createSync(recursive: true);
+
+  await Future.wait<void>(assetEntries.entries
+      .map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
+    var file = fileSystem.file(fileSystem.path.join(bundleDir.path, entry.key));
+    file.parent.createSync(recursive: true);
+    await file.writeAsBytes(await entry.value.contentsAsBytes());
+  }));
+}
diff --git a/frontend_server_common/lib/src/devfs_content.dart b/frontend_server_common/lib/src/devfs_content.dart
new file mode 100644
index 0000000..596f1ed
--- /dev/null
+++ b/frontend_server_common/lib/src/devfs_content.dart
@@ -0,0 +1,225 @@
+// Copyright 2020 The Dart Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Note: this is a copy from flutter tools, updated to work with dwds tests
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:file/file.dart';
+
+import 'utilities.dart';
+
+/// Common superclass for content copied to the device.
+abstract class DevFSContent {
+  /// Return true if this is the first time this method is called
+  /// or if the entry has been modified since this method was last called.
+  bool get isModified;
+
+  /// Return true if this is the first time this method is called
+  /// or if the entry has been modified after the given time
+  /// or if the given time is null.
+  bool isModifiedAfter(DateTime time);
+
+  int get size;
+
+  Future<List<int>> contentsAsBytes();
+
+  Stream<List<int>> contentsAsStream();
+
+  /// Return the list of files this content depends on.
+  List<String> get fileDependencies => <String>[];
+}
+
+// File content to be copied to the device.
+class DevFSFileContent extends DevFSContent {
+  DevFSFileContent(this.file);
+
+  final FileSystemEntity file;
+  File _linkTarget;
+  FileStat _fileStat;
+
+  File _getFile() {
+    if (_linkTarget != null) {
+      return _linkTarget;
+    }
+    if (file is Link) {
+      // The link target.
+      return fileSystem.file(file.resolveSymbolicLinksSync());
+    }
+    return file as File;
+  }
+
+  void _stat() {
+    if (_linkTarget != null) {
+      // Stat the cached symlink target.
+      var fileStat = _linkTarget.statSync();
+      if (fileStat.type == FileSystemEntityType.notFound) {
+        _linkTarget = null;
+      } else {
+        _fileStat = fileStat;
+        return;
+      }
+    }
+    var fileStat = file.statSync();
+    _fileStat =
+        fileStat.type == FileSystemEntityType.notFound ? null : fileStat;
+    if (_fileStat != null && _fileStat.type == FileSystemEntityType.link) {
+      // Resolve, stat the symlink target.
+      var resolved = file.resolveSymbolicLinksSync();
+      var linkTarget = fileSystem.file(resolved);
+      // Stat the link target.
+      var fileStat = linkTarget.statSync();
+      if (fileStat.type == FileSystemEntityType.notFound) {
+        _fileStat = null;
+        _linkTarget = null;
+      }
+    }
+    if (_fileStat == null) {
+      printError(
+          'Unable to get status of file "${file.path}": file not found.');
+    }
+  }
+
+  @override
+  List<String> get fileDependencies => <String>[_getFile().path];
+
+  @override
+  bool get isModified {
+    var _oldFileStat = _fileStat;
+    _stat();
+    if (_oldFileStat == null && _fileStat == null) {
+      return false;
+    }
+    return _oldFileStat == null ||
+        _fileStat == null ||
+        _fileStat.modified.isAfter(_oldFileStat.modified);
+  }
+
+  @override
+  bool isModifiedAfter(DateTime time) {
+    var _oldFileStat = _fileStat;
+    _stat();
+    if (_oldFileStat == null && _fileStat == null) {
+      return false;
+    }
+    return time == null ||
+        _oldFileStat == null ||
+        _fileStat == null ||
+        _fileStat.modified.isAfter(time);
+  }
+
+  @override
+  int get size {
+    if (_fileStat == null) {
+      _stat();
+    }
+    // Can still be null if the file wasn't found.
+    return _fileStat?.size ?? 0;
+  }
+
+  @override
+  Future<List<int>> contentsAsBytes() => _getFile().readAsBytes();
+
+  @override
+  Stream<List<int>> contentsAsStream() => _getFile().openRead();
+}
+
+/// Byte content to be copied to the device.
+class DevFSByteContent extends DevFSContent {
+  DevFSByteContent(this._bytes);
+
+  List<int> _bytes;
+
+  bool _isModified = true;
+  DateTime _modificationTime = DateTime.now();
+
+  List<int> get bytes => _bytes;
+
+  set bytes(List<int> value) {
+    _bytes = value;
+    _isModified = true;
+    _modificationTime = DateTime.now();
+  }
+
+  /// Return true only once so that the content is written to the device only once.
+  @override
+  bool get isModified {
+    var modified = _isModified;
+    _isModified = false;
+    return modified;
+  }
+
+  @override
+  bool isModifiedAfter(DateTime time) {
+    return time == null || _modificationTime.isAfter(time);
+  }
+
+  @override
+  int get size => _bytes.length;
+
+  @override
+  Future<List<int>> contentsAsBytes() async => _bytes;
+
+  @override
+  Stream<List<int>> contentsAsStream() =>
+      Stream<List<int>>.fromIterable(<List<int>>[_bytes]);
+}
+
+/// String content to be copied to the device.
+class DevFSStringContent extends DevFSByteContent {
+  DevFSStringContent(String string)
+      : _string = string,
+        super(utf8.encode(string));
+
+  String _string;
+
+  String get string => _string;
+
+  set string(String value) {
+    _string = value;
+    super.bytes = utf8.encode(_string);
+  }
+
+  @override
+  set bytes(List<int> value) {
+    string = utf8.decode(value);
+  }
+}
+
+// Basic statistics for DevFS update operation.
+class UpdateFSReport {
+  UpdateFSReport({
+    bool success = false,
+    int invalidatedSourcesCount = 0,
+    int syncedBytes = 0,
+  }) {
+    _success = success;
+    _invalidatedSourcesCount = invalidatedSourcesCount;
+    _syncedBytes = syncedBytes;
+  }
+
+  bool get success => _success;
+  int get invalidatedSourcesCount => _invalidatedSourcesCount;
+  int get syncedBytes => _syncedBytes;
+
+  /// JavaScript modules produced by the incremental compiler in `dartdevc`
+  /// mode.
+  ///
+  /// Only used for JavaScript compilation.
+  List<String> invalidatedModules;
+
+  void incorporateResults(UpdateFSReport report) {
+    if (!report._success) {
+      _success = false;
+    }
+    _invalidatedSourcesCount += report._invalidatedSourcesCount;
+    _syncedBytes += report._syncedBytes;
+    invalidatedModules ??= report.invalidatedModules;
+  }
+
+  bool _success;
+  int _invalidatedSourcesCount;
+  int _syncedBytes;
+}
diff --git a/frontend_server_common/lib/src/frontend_server_client.dart b/frontend_server_common/lib/src/frontend_server_client.dart
new file mode 100644
index 0000000..cd2182e
--- /dev/null
+++ b/frontend_server_common/lib/src/frontend_server_client.dart
@@ -0,0 +1,797 @@
+// Copyright 2020 The Dart Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Note: this is a copy from flutter tools, updated to work with dwds tests
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:dwds/dwds.dart';
+import 'package:path/path.dart' as p;
+import 'package:meta/meta.dart';
+import 'package:pedantic/pedantic.dart';
+import 'package:usage/uuid/uuid.dart';
+import 'package:logging/logging.dart';
+
+import 'package_map.dart';
+import 'utilities.dart';
+
+/// True iff debug mode
+bool get debugFrontendServer =>
+    Platform.environment['DWDS_DEBUG_FRONTEND_SERVER'] == 'true';
+
+/// In debug mode, path to frontend_server_starter.dart
+/// Otherwise, path to precompiled snapshot.
+/// In debug mode, frontend_server prints an observatory Uri to stderr.
+String get frontendServerExecutable {
+  if (debugFrontendServer) {
+    final starter = Platform.environment['DWDS_FRONTEND_SERVER_STARTER'];
+    if (starter == null) {
+      throw Exception('Debug mode - define DWDS_FRONTEND_SERVER_STARTER '
+          'environment variable to point to frontend_server_starter.dart');
+    }
+    return starter;
+  }
+  final snapshot =
+      p.join(dartSdkPath, 'bin', 'snapshots', 'frontend_server.dart.snapshot');
+  return snapshot;
+}
+
+typedef CompilerMessageConsumer = void Function(String message,
+    {StackTrace stackTrace});
+
+class CompilerOutput {
+  const CompilerOutput(this.outputFilename, this.errorCount, this.sources);
+
+  final String outputFilename;
+  final int errorCount;
+  final List<Uri> sources;
+}
+
+enum StdoutState { collectDiagnostic, collectDependencies }
+
+/// Handles stdin/stdout communication with the frontend server.
+class StdoutHandler {
+  final LogWriter _logWriter;
+
+  void _printTrace(String message) {
+    _logWriter(Level.INFO, message);
+  }
+
+  StdoutHandler(this._logWriter, {this.consumer = printError}) {
+    reset();
+  }
+
+  bool compilerMessageReceived = false;
+  final CompilerMessageConsumer consumer;
+  String boundaryKey;
+  StdoutState state = StdoutState.collectDiagnostic;
+  Completer<CompilerOutput> compilerOutput;
+  final List<Uri> sources = <Uri>[];
+
+  bool _suppressCompilerMessages;
+  bool _expectSources;
+  bool _badState = false;
+
+  void handler(String message) {
+    if (message.startsWith('Observatory listening')) {
+      stderr.writeln(message);
+      return;
+    }
+    if (message.startsWith('Observatory server failed')) {
+      throw Exception(message);
+    }
+    if (_badState) {
+      return;
+    }
+    var kResultPrefix = 'result ';
+    if (boundaryKey == null && message.startsWith(kResultPrefix)) {
+      boundaryKey = message.substring(kResultPrefix.length);
+      return;
+    }
+    // Invalid state, see commented issue below for more information.
+    // NB: both the completeError and _badState flags are required to avoid
+    // filling the console with exceptions.
+    if (boundaryKey == null) {
+      // Throwing a synchronous exception via throwToolExit will fail to cancel
+      // the stream. Instead use completeError so that the error is returned
+      // from the awaited future that the compiler consumers are expecting.
+      compilerOutput.completeError(
+          'Frontend server tests encountered an internal problem'
+          'This can be caused by printing to stdout into the stream that is '
+          'used for communication between frontend server (in sdk) or '
+          'frontend server client (in dwds tests).'
+          '\n\n'
+          'Additional debugging information:\n'
+          '  StdoutState: $state\n'
+          '  compilerMessageReceived: $compilerMessageReceived\n'
+          '  message: $message\n'
+          '  _expectSources: $_expectSources\n'
+          '  sources: $sources\n');
+      // There are several event turns before the tool actually exits from a
+      // tool exception. Normally, the stream should be cancelled to prevent
+      // more events from entering the bad state, but because the error
+      // is coming from handler itself, there is no clean way to pipe this
+      // through. Instead, we set a flag to prevent more messages from
+      // registering.
+      _badState = true;
+      return;
+    }
+    if (message.startsWith(boundaryKey)) {
+      if (_expectSources) {
+        if (state == StdoutState.collectDiagnostic) {
+          state = StdoutState.collectDependencies;
+          return;
+        }
+      }
+      if (message.length <= boundaryKey.length) {
+        compilerOutput.complete(null);
+        return;
+      }
+      var spaceDelimiter = message.lastIndexOf(' ');
+      compilerOutput.complete(CompilerOutput(
+          message.substring(boundaryKey.length + 1, spaceDelimiter),
+          int.parse(message.substring(spaceDelimiter + 1).trim()),
+          sources));
+      return;
+    }
+    if (state == StdoutState.collectDiagnostic) {
+      if (!_suppressCompilerMessages) {
+        if (compilerMessageReceived == false) {
+          consumer('\nCompiler message:');
+          compilerMessageReceived = true;
+        }
+        consumer(message);
+      }
+    } else {
+      assert(state == StdoutState.collectDependencies);
+      switch (message[0]) {
+        case '+':
+          sources.add(Uri.parse(message.substring(1)));
+          break;
+        case '-':
+          sources.remove(Uri.parse(message.substring(1)));
+          break;
+        default:
+          _printTrace('Unexpected prefix for $message uri - ignoring');
+      }
+    }
+  }
+
+  // This is needed to get ready to process next compilation result output,
+  // with its own boundary key and new completer.
+  void reset(
+      {bool suppressCompilerMessages = false, bool expectSources = true}) {
+    boundaryKey = null;
+    compilerMessageReceived = false;
+    compilerOutput = Completer<CompilerOutput>();
+    _suppressCompilerMessages = suppressCompilerMessages;
+    _expectSources = expectSources;
+    state = StdoutState.collectDiagnostic;
+  }
+}
+
+/// Converts filesystem paths to package URIs.
+class PackageUriMapper {
+  PackageUriMapper(String scriptPath, String packagesFilePath,
+      String fileSystemScheme, List<String> fileSystemRoots) {
+    var packageMap = PackageMap(fileSystem.path.absolute(packagesFilePath)).map;
+    var isWindowsPath =
+        Platform.isWindows && !scriptPath.startsWith('org-dartlang-app');
+    var scriptUri = Uri.file(scriptPath, windows: isWindowsPath).toString();
+    for (var packageName in packageMap.keys) {
+      var prefix = packageMap[packageName].toString();
+      // Only perform a multi-root mapping if there are multiple roots.
+      if (fileSystemScheme != null &&
+          fileSystemRoots != null &&
+          fileSystemRoots.length > 1 &&
+          prefix.contains(fileSystemScheme)) {
+        _packageName = packageName;
+        _uriPrefixes = fileSystemRoots
+            .map((String name) =>
+                Uri.file(name, windows: Platform.isWindows).toString())
+            .toList();
+        return;
+      }
+      if (scriptUri.startsWith(prefix)) {
+        _packageName = packageName;
+        _uriPrefixes = <String>[prefix];
+        return;
+      }
+    }
+  }
+
+  String _packageName;
+  List<String> _uriPrefixes;
+
+  Uri map(String scriptPath) {
+    if (_packageName == null) {
+      return null;
+    }
+    var scriptUri =
+        Uri.file(scriptPath, windows: Platform.isWindows).toString();
+    for (var uriPrefix in _uriPrefixes) {
+      if (scriptUri.startsWith(uriPrefix)) {
+        return Uri.parse(
+            'package:$_packageName/${scriptUri.substring(uriPrefix.length)}');
+      }
+    }
+    return null;
+  }
+
+  static Uri findUri(String scriptPath, String packagesFilePath,
+      String fileSystemScheme, List<String> fileSystemRoots) {
+    return PackageUriMapper(
+            scriptPath, packagesFilePath, fileSystemScheme, fileSystemRoots)
+        .map(scriptPath);
+  }
+}
+
+/// Class that allows to serialize compilation requests to the compiler.
+abstract class _CompilationRequest {
+  _CompilationRequest(this.completer);
+
+  Completer<CompilerOutput> completer;
+
+  Future<CompilerOutput> _run(DefaultResidentCompiler compiler);
+
+  Future<void> run(DefaultResidentCompiler compiler) async {
+    completer.complete(await _run(compiler));
+  }
+}
+
+class _RecompileRequest extends _CompilationRequest {
+  _RecompileRequest(
+    Completer<CompilerOutput> completer,
+    this.mainPath,
+    this.invalidatedFiles,
+    this.outputPath,
+    this.packagesFilePath,
+  ) : super(completer);
+
+  String mainPath;
+  List<Uri> invalidatedFiles;
+  String outputPath;
+  String packagesFilePath;
+
+  @override
+  Future<CompilerOutput> _run(DefaultResidentCompiler compiler) async =>
+      compiler._recompile(this);
+}
+
+class _CompileExpressionRequest extends _CompilationRequest {
+  _CompileExpressionRequest(
+    Completer<CompilerOutput> completer,
+    this.expression,
+    this.definitions,
+    this.typeDefinitions,
+    this.libraryUri,
+    this.klass,
+    this.isStatic,
+  ) : super(completer);
+
+  String expression;
+  List<String> definitions;
+  List<String> typeDefinitions;
+  String libraryUri;
+  String klass;
+  bool isStatic;
+
+  @override
+  Future<CompilerOutput> _run(DefaultResidentCompiler compiler) async =>
+      compiler._compileExpression(this);
+}
+
+class _CompileExpressionToJsRequest extends _CompilationRequest {
+  _CompileExpressionToJsRequest(
+      Completer<CompilerOutput> completer,
+      this.libraryUri,
+      this.line,
+      this.column,
+      this.jsModules,
+      this.jsFrameValues,
+      this.moduleName,
+      this.expression)
+      : super(completer);
+
+  String libraryUri;
+  int line;
+  int column;
+  Map<String, String> jsModules;
+  Map<String, String> jsFrameValues;
+  String moduleName;
+  String expression;
+
+  @override
+  Future<CompilerOutput> _run(DefaultResidentCompiler compiler) async =>
+      compiler._compileExpressionToJs(this);
+}
+
+class _RejectRequest extends _CompilationRequest {
+  _RejectRequest(Completer<CompilerOutput> completer) : super(completer);
+
+  @override
+  Future<CompilerOutput> _run(DefaultResidentCompiler compiler) async =>
+      compiler._reject();
+}
+
+/// Wrapper around incremental frontend server compiler, that communicates with
+/// server via stdin/stdout.
+///
+/// The wrapper is intended to stay resident in memory as user changes, reloads,
+/// restarts the Flutter app.
+abstract class ResidentCompiler {
+  factory ResidentCompiler(
+    String sdkRoot,
+    LogWriter logWriter, {
+    String packagesPath,
+    List<String> fileSystemRoots,
+    String fileSystemScheme,
+    String platformDill,
+    CompilerMessageConsumer compilerMessageConsumer,
+  }) = DefaultResidentCompiler;
+
+  // TODO(jonahwilliams): find a better way to configure additional file system
+  // roots from the runner.
+  // See: https://github.com/flutter/flutter/issues/50494
+  void addFileSystemRoot(String root);
+
+  /// If invoked for the first time, it compiles Dart script identified by
+  /// [mainPath], [invalidatedFiles] list is ignored.
+  /// On successive runs [invalidatedFiles] indicates which files need to be
+  /// recompiled. If [mainPath] is null, previously used [mainPath] entry
+  /// point that is used for recompilation.
+  /// Binary file name is returned if compilation was successful, otherwise
+  /// null is returned.
+  Future<CompilerOutput> recompile(
+    String mainPath,
+    List<Uri> invalidatedFiles, {
+    @required String outputPath,
+    String packagesFilePath,
+  });
+
+  Future<CompilerOutput> compileExpression(
+    String expression,
+    List<String> definitions,
+    List<String> typeDefinitions,
+    String libraryUri,
+    String klass,
+    bool isStatic,
+  );
+
+  Future<CompilerOutput> compileExpressionToJs(
+      String libraryUri,
+      int line,
+      int column,
+      Map<String, String> jsModules,
+      Map<String, String> jsFrameValues,
+      String moduleName,
+      String expression);
+
+  /// Should be invoked when results of compilation are accepted by the client.
+  ///
+  /// Either [accept] or [reject] should be called after every [recompile] call.
+  void accept();
+
+  /// Should be invoked when results of compilation are rejected by the client.
+  ///
+  /// Either [accept] or [reject] should be called after every [recompile] call.
+  Future<CompilerOutput> reject();
+
+  /// Should be invoked when frontend server compiler should forget what was
+  /// accepted previously so that next call to [recompile] produces complete
+  /// kernel file.
+  void reset();
+
+  /// stop the service normally
+  Future<dynamic> shutdown();
+
+  /// kill the service
+  Future<dynamic> kill();
+}
+
+@visibleForTesting
+class DefaultResidentCompiler implements ResidentCompiler {
+  DefaultResidentCompiler(
+    String sdkRoot,
+    this._logWriter, {
+    this.packagesPath,
+    this.fileSystemRoots,
+    this.fileSystemScheme,
+    this.platformDill,
+    CompilerMessageConsumer compilerMessageConsumer = printError,
+  })  : assert(sdkRoot != null),
+        _stdoutHandler =
+            StdoutHandler(_logWriter, consumer: compilerMessageConsumer),
+        // This is a URI, not a file path, so the forward slash is correct even on Windows.
+        sdkRoot = sdkRoot.endsWith('/') ? sdkRoot : '$sdkRoot/';
+
+  final LogWriter _logWriter;
+  final String packagesPath;
+  final List<String> fileSystemRoots;
+  final String fileSystemScheme;
+  final String platformDill;
+
+  void _printTrace(String message) {
+    _logWriter(Level.INFO, message);
+  }
+
+  @override
+  void addFileSystemRoot(String root) {
+    fileSystemRoots.add(root);
+  }
+
+  /// The path to the root of the Dart SDK used to compile.
+  final String sdkRoot;
+
+  Process _server;
+  final StdoutHandler _stdoutHandler;
+  bool _compileRequestNeedsConfirmation = false;
+
+  final StreamController<_CompilationRequest> _controller =
+      StreamController<_CompilationRequest>();
+
+  @override
+  Future<CompilerOutput> recompile(String mainPath, List<Uri> invalidatedFiles,
+      {@required String outputPath, String packagesFilePath}) async {
+    assert(outputPath != null);
+    if (!_controller.hasListener) {
+      _controller.stream.listen(_handleCompilationRequest);
+    }
+
+    var completer = Completer<CompilerOutput>();
+    _controller.add(_RecompileRequest(
+        completer, mainPath, invalidatedFiles, outputPath, packagesFilePath));
+    return completer.future;
+  }
+
+  Future<CompilerOutput> _recompile(_RecompileRequest request) async {
+    _stdoutHandler.reset();
+
+    // First time recompile is called we actually have to compile the app from
+    // scratch ignoring list of invalidated files.
+    PackageUriMapper packageUriMapper;
+    if (request.packagesFilePath != null) {
+      packageUriMapper = PackageUriMapper(
+        request.mainPath,
+        request.packagesFilePath,
+        fileSystemScheme,
+        fileSystemRoots,
+      );
+    }
+
+    _compileRequestNeedsConfirmation = true;
+
+    if (_server == null) {
+      return _compile(_mapFilename(request.mainPath, packageUriMapper),
+          request.outputPath, request.packagesFilePath);
+    }
+
+    var inputKey = Uuid().generateV4();
+    var mainUri = request.mainPath != null
+        ? '${_mapFilename(request.mainPath, packageUriMapper)} '
+        : '';
+    _server.stdin.writeln('recompile $mainUri$inputKey');
+    _printTrace('<- recompile $mainUri$inputKey');
+    for (var fileUri in request.invalidatedFiles) {
+      var message = _mapFileUri(fileUri.toString(), packageUriMapper);
+      _server.stdin.writeln(message);
+      _printTrace(message);
+    }
+    _server.stdin.writeln(inputKey);
+    _printTrace('<- $inputKey');
+
+    return _stdoutHandler.compilerOutput.future;
+  }
+
+  final List<_CompilationRequest> _compilationQueue = <_CompilationRequest>[];
+
+  Future<void> _handleCompilationRequest(_CompilationRequest request) async {
+    var isEmpty = _compilationQueue.isEmpty;
+    _compilationQueue.add(request);
+    // Only trigger processing if queue was empty - i.e. no other requests
+    // are currently being processed. This effectively enforces "one
+    // compilation request at a time".
+    if (isEmpty) {
+      while (_compilationQueue.isNotEmpty) {
+        var request = _compilationQueue.first;
+        await request.run(this);
+        _compilationQueue.removeAt(0);
+      }
+    }
+  }
+
+  Future<CompilerOutput> _compile(
+      String scriptUri, String outputFilePath, String packagesFilePath) async {
+    var debug = debugFrontendServer;
+    var frontendServer = frontendServerExecutable;
+    var args = <String>[
+      if (debug) '--observe',
+      frontendServer,
+      '--sdk-root',
+      sdkRoot,
+      '--incremental',
+      '--target=dartdevc',
+      '-Ddart.developer.causal_async_stacks=true',
+      '--output-dill',
+      outputFilePath,
+      if (packagesFilePath != null) ...<String>[
+        '--packages',
+        packagesFilePath,
+      ] else if (packagesPath != null) ...<String>[
+        '--packages',
+        packagesPath,
+      ],
+      if (fileSystemRoots != null)
+        for (final String root in fileSystemRoots) ...<String>[
+          '--filesystem-root',
+          root,
+        ],
+      if (fileSystemScheme != null) ...<String>[
+        '--filesystem-scheme',
+        fileSystemScheme,
+      ],
+      if (platformDill != null) ...<String>[
+        '--platform',
+        platformDill,
+      ],
+      '--debugger-module-names',
+      if (debug) '--verbose'
+    ];
+
+    _printTrace(args.join(' '));
+    _server = await Process.start(Platform.resolvedExecutable, args,
+        workingDirectory: packagesPath);
+    _server.stdout
+        .transform<String>(utf8.decoder)
+        .transform<String>(const LineSplitter())
+        .listen(_stdoutHandler.handler, onDone: () {
+      // when outputFilename future is not completed, but stdout is closed
+      // process has died unexpectedly.
+      if (!_stdoutHandler.compilerOutput.isCompleted) {
+        _stdoutHandler.compilerOutput.complete(null);
+        throw Exception('the Dart compiler exited unexpectedly.');
+      }
+    });
+
+    _server.stderr
+        .transform<String>(utf8.decoder)
+        .transform<String>(const LineSplitter())
+        .listen(printError);
+
+    unawaited(_server.exitCode.then((int code) {
+      if (code != 0) {
+        throw Exception('the Dart compiler exited unexpectedly.');
+      }
+    }));
+
+    _server.stdin.writeln('compile $scriptUri');
+    _printTrace('<- compile $scriptUri');
+
+    return _stdoutHandler.compilerOutput.future;
+  }
+
+  @override
+  Future<CompilerOutput> compileExpression(
+    String expression,
+    List<String> definitions,
+    List<String> typeDefinitions,
+    String libraryUri,
+    String klass,
+    bool isStatic,
+  ) {
+    if (!_controller.hasListener) {
+      _controller.stream.listen(_handleCompilationRequest);
+    }
+
+    var completer = Completer<CompilerOutput>();
+    _controller.add(_CompileExpressionRequest(completer, expression,
+        definitions, typeDefinitions, libraryUri, klass, isStatic));
+    return completer.future;
+  }
+
+  Future<CompilerOutput> _compileExpression(
+      _CompileExpressionRequest request) async {
+    _stdoutHandler.reset(suppressCompilerMessages: true, expectSources: false);
+
+    // 'compile-expression' should be invoked after compiler has been started,
+    // program was compiled.
+    if (_server == null) {
+      return null;
+    }
+
+    var inputKey = Uuid().generateV4();
+    _server.stdin.writeln('compile-expression $inputKey');
+    _server.stdin.writeln(request.expression);
+    request.definitions?.forEach(_server.stdin.writeln);
+    _server.stdin.writeln(inputKey);
+    request.typeDefinitions?.forEach(_server.stdin.writeln);
+    _server.stdin.writeln(inputKey);
+    _server.stdin.writeln(request.libraryUri ?? '');
+    _server.stdin.writeln(request.klass ?? '');
+    _server.stdin.writeln(request.isStatic ?? false);
+
+    return _stdoutHandler.compilerOutput.future;
+  }
+
+  @override
+  Future<CompilerOutput> compileExpressionToJs(
+      String libraryUri,
+      int line,
+      int column,
+      Map<String, String> jsModules,
+      Map<String, String> jsFrameValues,
+      String moduleName,
+      String expression) {
+    if (!_controller.hasListener) {
+      _controller.stream.listen(_handleCompilationRequest);
+    }
+
+    var completer = Completer<CompilerOutput>();
+    _controller.add(_CompileExpressionToJsRequest(completer, libraryUri, line,
+        column, jsModules, jsFrameValues, moduleName, expression));
+    return completer.future;
+  }
+
+  Future<CompilerOutput> _compileExpressionToJs(
+      _CompileExpressionToJsRequest request) async {
+    _stdoutHandler.reset(suppressCompilerMessages: true, expectSources: false);
+
+    // 'compile-expression-to-js' should be invoked after compiler has been started,
+    // program was compiled.
+    if (_server == null) {
+      return null;
+    }
+
+    var inputKey = Uuid().generateV4();
+    _server.stdin.writeln('compile-expression-to-js $inputKey');
+    _server.stdin.writeln(request.libraryUri ?? '');
+    _server.stdin.writeln(request.line);
+    _server.stdin.writeln(request.column);
+    request.jsModules?.forEach((k, v) {
+      _server.stdin.writeln('$k:$v');
+    });
+    _server.stdin.writeln(inputKey);
+    request.jsFrameValues?.forEach((k, v) {
+      _server.stdin.writeln('$k:$v');
+    });
+    _server.stdin.writeln(inputKey);
+    _server.stdin.writeln(request.moduleName ?? '');
+    _server.stdin.writeln(request.expression ?? '');
+
+    return _stdoutHandler.compilerOutput.future;
+  }
+
+  @override
+  void accept() {
+    if (_compileRequestNeedsConfirmation) {
+      _server.stdin.writeln('accept');
+      _printTrace('<- accept');
+    }
+    _compileRequestNeedsConfirmation = false;
+  }
+
+  @override
+  Future<CompilerOutput> reject() {
+    if (!_controller.hasListener) {
+      _controller.stream.listen(_handleCompilationRequest);
+    }
+
+    var completer = Completer<CompilerOutput>();
+    _controller.add(_RejectRequest(completer));
+    return completer.future;
+  }
+
+  Future<CompilerOutput> _reject() {
+    if (!_compileRequestNeedsConfirmation) {
+      return Future<CompilerOutput>.value(null);
+    }
+    _stdoutHandler.reset(expectSources: false);
+    _server.stdin.writeln('reject');
+    _printTrace('<- reject');
+    _compileRequestNeedsConfirmation = false;
+    return _stdoutHandler.compilerOutput.future;
+  }
+
+  @override
+  void reset() {
+    _server?.stdin?.writeln('reset');
+    _printTrace('<- reset');
+  }
+
+  Future<int> quit() async {
+    _server.stdin.writeln('quit');
+    _printTrace('<- quit');
+    return _server.exitCode;
+  }
+
+  String _mapFilename(String filename, PackageUriMapper packageUriMapper) {
+    return _doMapFilename(filename, packageUriMapper) ?? filename;
+  }
+
+  String _mapFileUri(String fileUri, PackageUriMapper packageUriMapper) {
+    String filename;
+    try {
+      filename = Uri.parse(fileUri).toFilePath();
+    } on UnsupportedError catch (_) {
+      return fileUri;
+    }
+    return _doMapFilename(filename, packageUriMapper) ?? fileUri;
+  }
+
+  String _doMapFilename(String filename, PackageUriMapper packageUriMapper) {
+    if (packageUriMapper != null) {
+      var packageUri = packageUriMapper.map(filename);
+      if (packageUri != null) {
+        return packageUri.toString();
+      }
+    }
+
+    if (fileSystemRoots != null) {
+      for (var root in fileSystemRoots) {
+        if (filename.startsWith(root)) {
+          return Uri(
+                  scheme: fileSystemScheme,
+                  path: filename.substring(root.length))
+              .toString();
+        }
+      }
+    }
+    if (Platform.isWindows &&
+        fileSystemRoots != null &&
+        fileSystemRoots.length > 1) {
+      return Uri.file(filename, windows: Platform.isWindows).toString();
+    }
+    return null;
+  }
+
+  @override
+  Future<dynamic> shutdown() async {
+    // Server was never successfully created.
+    if (_server == null) {
+      return 0;
+    }
+    return quit();
+  }
+
+  @override
+  Future<dynamic> kill() async {
+    if (_server == null) {
+      return 0;
+    }
+
+    _printTrace('killing pid ${_server.pid}');
+    _server.kill();
+    return _server.exitCode;
+  }
+}
+
+class TestExpressionCompiler implements ExpressionCompiler {
+  final ResidentCompiler _generator;
+  TestExpressionCompiler(this._generator);
+
+  @override
+  Future<ExpressionCompilationResult> compileExpressionToJs(
+      String isolateId,
+      String libraryUri,
+      int line,
+      int column,
+      Map<String, String> jsModules,
+      Map<String, String> jsFrameValues,
+      String moduleName,
+      String expression) async {
+    var compilerOutput = await _generator.compileExpressionToJs(libraryUri,
+        line, column, jsModules, jsFrameValues, moduleName, expression);
+
+    if (compilerOutput != null && compilerOutput.outputFilename != null) {
+      var content = utf8.decode(
+          fileSystem.file(compilerOutput.outputFilename).readAsBytesSync());
+      return ExpressionCompilationResult(
+          content, compilerOutput.errorCount > 0);
+    }
+
+    throw Exception('Failed to compile $expression');
+  }
+}
diff --git a/frontend_server_common/lib/src/package_map.dart b/frontend_server_common/lib/src/package_map.dart
new file mode 100644
index 0000000..bb6ee21
--- /dev/null
+++ b/frontend_server_common/lib/src/package_map.dart
@@ -0,0 +1,84 @@
+// Copyright 2020 The Dart Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Note: this is a copy from flutter tools
+
+// TODO(annagrin): unify this code so both repos can use it
+// [issue #40835] (https://github.com/dart-lang/sdk/issues/40835)
+
+import 'dart:io';
+
+// ignore: deprecated_member_use
+import 'package:package_config/packages_file.dart' as packages_file;
+
+import 'utilities.dart';
+
+const String kPackagesFileName = '.packages';
+
+Map<String, Uri> _parse(String packagesPath) {
+  final List<int> source = fileSystem.file(packagesPath).readAsBytesSync();
+  return packages_file.parse(
+      source, Uri.file(packagesPath, windows: Platform.isWindows));
+}
+
+class PackageMap {
+  PackageMap(this.packagesPath);
+
+  static String get globalPackagesPath =>
+      _globalPackagesPath ?? kPackagesFileName;
+
+  static set globalPackagesPath(String value) {
+    _globalPackagesPath = value;
+  }
+
+  static bool get isUsingCustomPackagesPath => _globalPackagesPath != null;
+
+  static String _globalPackagesPath;
+
+  final String packagesPath;
+
+  /// Load and parses the .packages file.
+  void load() {
+    _map ??= _parse(packagesPath);
+  }
+
+  Map<String, Uri> get map {
+    load();
+    return _map;
+  }
+
+  Map<String, Uri> _map;
+
+  /// Returns the path to [packageUri].
+  String pathForPackage(Uri packageUri) => uriForPackage(packageUri).path;
+
+  /// Returns the path to [packageUri] as URL.
+  Uri uriForPackage(Uri packageUri) {
+    assert(packageUri.scheme == 'package');
+    var pathSegments = packageUri.pathSegments.toList();
+    var packageName = pathSegments.removeAt(0);
+    var packageBase = map[packageName];
+    if (packageBase == null) {
+      return null;
+    }
+    var packageRelativePath = fileSystem.path.joinAll(pathSegments);
+    return packageBase.resolveUri(fileSystem.path.toUri(packageRelativePath));
+  }
+
+  String checkValid() {
+    if (fileSystem.isFileSync(packagesPath)) {
+      return null;
+    }
+    var message = '$packagesPath does not exist.';
+    var pubspecPath = fileSystem.path
+        .absolute(fileSystem.path.dirname(packagesPath), 'pubspec.yaml');
+    if (fileSystem.isFileSync(pubspecPath)) {
+      message += '\nDid you run "flutter pub get" in this directory?';
+    } else {
+      message +=
+          '\nDid you run this command from the same directory as your pubspec.yaml file?';
+    }
+    return message;
+  }
+}
diff --git a/frontend_server_common/lib/src/resident_runner.dart b/frontend_server_common/lib/src/resident_runner.dart
new file mode 100644
index 0000000..979bb1e
--- /dev/null
+++ b/frontend_server_common/lib/src/resident_runner.dart
@@ -0,0 +1,102 @@
+// Copyright 2020 The Dart Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Note: this is a copy from flutter tools, updated to work with dwds tests,
+// and some functionality remioved (does not support hot reload yet)
+
+import 'dart:async';
+
+import 'package:dwds/dwds.dart';
+import 'package:path/path.dart' as p;
+
+import 'asset.dart';
+import 'devfs.dart';
+import 'devfs_content.dart';
+import 'frontend_server_client.dart';
+import 'utilities.dart';
+
+final String platformDill = p.join(dartSdkPath, '..', 'ddc_sdk.dill');
+
+class ResidentWebRunner {
+  ResidentWebRunner(
+      this.mainPath,
+      this.urlTunneller,
+      this.packagesPath,
+      this.packagesFilePath,
+      this.fileSystemRoots,
+      this.fileSystemScheme,
+      this.outputPath,
+      this.logWriter) {
+    generator = ResidentCompiler(dartSdkPath, logWriter,
+        packagesPath: packagesPath,
+        platformDill: platformDill,
+        fileSystemRoots: fileSystemRoots,
+        fileSystemScheme: fileSystemScheme);
+    expressionCompiler = TestExpressionCompiler(generator);
+  }
+
+  final LogWriter logWriter;
+  final UrlEncoder urlTunneller;
+  final String mainPath;
+  final String packagesPath;
+  final String packagesFilePath;
+  final String outputPath;
+  final List<String> fileSystemRoots;
+  final String fileSystemScheme;
+
+  ResidentCompiler generator;
+  ExpressionCompiler expressionCompiler;
+  AssetBundle assetBundle;
+  WebDevFS devFS;
+  Uri uri;
+  Iterable<String> modules;
+
+  Future<int> run(String hostname, int port, String root) async {
+    hostname ??= 'localhost';
+
+    assetBundle = AssetBundleFactory.defaultInstance.createBundle();
+
+    devFS = WebDevFS(
+        fileSystem: fileSystem,
+        hostname: hostname,
+        port: port,
+        packagesFilePath: packagesFilePath,
+        packagesPath: packagesPath,
+        root: root,
+        urlTunneller: urlTunneller,
+        logWriter: logWriter);
+    uri = await devFS.create();
+
+    var report = await _updateDevFS();
+    if (!report.success) {
+      printError('Failed to compile application.');
+      return 1;
+    }
+
+    modules = report.invalidatedModules;
+
+    generator.accept();
+    return 0;
+  }
+
+  Future<UpdateFSReport> _updateDevFS() async {
+    var result = await assetBundle.build();
+    if (result != 0) {
+      return UpdateFSReport(success: false);
+    }
+
+    var report = await devFS.update(
+        mainPath: mainPath,
+        bundle: assetBundle,
+        dillOutputPath: outputPath,
+        generator: generator,
+        invalidatedFiles: []);
+    return report;
+  }
+
+  Future<void> stop() async {
+    await generator.shutdown();
+    await devFS.dispose();
+  }
+}
diff --git a/frontend_server_common/lib/src/utilities.dart b/frontend_server_common/lib/src/utilities.dart
new file mode 100644
index 0000000..ab4e324
--- /dev/null
+++ b/frontend_server_common/lib/src/utilities.dart
@@ -0,0 +1,37 @@
+// 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.
+
+import 'dart:io';
+
+import 'package:file/file.dart' as fs;
+import 'package:file/local.dart';
+import 'package:logging/logging.dart';
+import 'package:path/path.dart' as p;
+
+typedef LogWriter = void Function(Level, String);
+
+/// The path to the root directory of the SDK.
+final String _sdkDir = (() {
+  // The Dart executable is in "/path/to/sdk/bin/dart", so two levels up is
+  // "/path/to/sdk".
+  var aboveExecutable = p.dirname(p.dirname(Platform.resolvedExecutable));
+  assert(FileSystemEntity.isFileSync(p.join(aboveExecutable, 'version')));
+  return aboveExecutable;
+})();
+
+final String dartSdkPath = _sdkDir;
+final String dartPath = p.join(_sdkDir, 'bin', 'dart');
+final String pubSnapshot =
+    p.join(_sdkDir, 'bin', 'snapshots', 'pub.dart.snapshot');
+final String pubPath =
+    p.join(_sdkDir, 'bin', Platform.isWindows ? 'pub.bat' : 'pub');
+
+const fs.FileSystem fileSystem = LocalFileSystem();
+
+void printError(String message, {StackTrace stackTrace}) {
+  if (stackTrace != null) {
+    print('$message: $stackTrace');
+  }
+  print(message);
+}
diff --git a/frontend_server_common/mono_pkg.yaml b/frontend_server_common/mono_pkg.yaml
new file mode 100644
index 0000000..8e98c68
--- /dev/null
+++ b/frontend_server_common/mono_pkg.yaml
@@ -0,0 +1,14 @@
+# See https://github.com/dart-lang/mono_repo for details
+dart:
+  - 2.6.0
+  - dev 
+
+stages:
+  - analyzer_and_format:
+    - group:
+      - dartfmt
+      - dartanalyzer: --fatal-infos --fatal-warnings .
+      dart: dev
+    - group:
+      - dartanalyzer: --fatal-warnings .
+      dart: [2.6.0]
diff --git a/frontend_server_common/pubspec.yaml b/frontend_server_common/pubspec.yaml
new file mode 100644
index 0000000..d3cf8ef
--- /dev/null
+++ b/frontend_server_common/pubspec.yaml
@@ -0,0 +1,13 @@
+name: frontend_server_common
+publish_to: none
+description: >-
+  Frontend server integration code to use for dwds tests. Mimicks flutter code.
+environment:
+  sdk: ">=2.6.0 <3.0.0"
+
+dependencies:
+  dwds:
+    path: ../dwds
+
+
+
diff --git a/tool/travis.sh b/tool/travis.sh
index 254fb61..9968b40 100755
--- a/tool/travis.sh
+++ b/tool/travis.sh
@@ -75,14 +75,10 @@
       pub run test -x frontend-server || EXIT_CODE=$?
       ;;
     test_2)
-      echo 'pub run test'
-      pub run test || EXIT_CODE=$?
-      ;;
-    test_3)
       echo 'pub run test test/build/ensure_build_test.dart'
       pub run test test/build/ensure_build_test.dart || EXIT_CODE=$?
       ;;
-    test_4)
+    test_3)
       echo 'pub run test -j 1'
       pub run test -j 1 || EXIT_CODE=$?
       ;;