diff --git a/packages/flutter_tools/lib/src/web/bootstrap.dart b/packages/flutter_tools/lib/src/web/bootstrap.dart new file mode 100644 index 00000000000..247e56d7207 --- /dev/null +++ b/packages/flutter_tools/lib/src/web/bootstrap.dart @@ -0,0 +1,100 @@ +// Copyright 2019 The Chromium 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: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 bootstraping 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"); +document.head.appendChild(requireEl); + +// Invoked by connected chrome debugger for hot reload/restart support. +window.\$hotReloadHook = function(modules) { + return new Promise(function(resolve, reject) { + if (modules == null) { + reject(); + } + // If no modules change, return immediately. + if (modules.length == 0) { + resolve(); + } + var reloadCount = 0; + for (var i = 0; i < modules.length; i++) { + require.undef(modules[i]); + require([modules[i]], function(module) { + reloadCount += 1; + // once we've reloaded every module, trigger the hot reload. + if (reloadCount == modules.length) { + require(["$entrypoint", "dart_sdk"], function(app, dart_sdk) { + window.\$mainEntrypoint = app.main.main; + window.\$hotReload(resolve); + }); + } + }); + } + }); +} +'''; +} + +/// Generate a synthetic main module which captures the application's main +/// method. +String generateMainModule({@required String entrypoint}) { + return ''' +// Create the main module loaded below. +define("main_module", ["$entrypoint", "dart_sdk"], function(app, dart_sdk) { + dart_sdk.dart.setStartAsyncSynchronously(true); + dart_sdk._isolate_helper.startRootIsolate(() => {}, []); + dart_sdk._debugger.registerDevtoolsFormatter(); + dart_sdk.ui.webOnlyInitializePlatform(); + + // Attach the main entrypoint and hot reload functionality to the window. + window.\$mainEntrypoint = app.main.main; + if (window.\$hotReload == null) { + window.\$hotReload = function(cb) { + dart_sdk.developer.invokeExtension("ext.flutter.disassemble", "{}"); + dart_sdk.dart.hotRestart(); + window.\$mainEntrypoint(); + if (cb != null) { + cb(); + } + } + } + app.main.main(); +}); + +// Require JS configuration. +require.config({ + waitSeconds: 0, +}); +'''; +} diff --git a/packages/flutter_tools/test/general.shard/web/bootstrap_test.dart b/packages/flutter_tools/test/general.shard/web/bootstrap_test.dart new file mode 100644 index 00000000000..6b2be800b68 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/web/bootstrap_test.dart @@ -0,0 +1,35 @@ +// Copyright 2019 The Chromium 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:flutter_tools/src/web/bootstrap.dart'; + +import '../../src/common.dart'; + +void main() { + test('generateBootstrapScript embeds urls correctly', () { + final String result = generateBootstrapScript( + requireUrl: 'require.js', + mapperUrl: 'mapper.js', + entrypoint: 'foo/bar/main.js', + ); + // require js source is interpolated correctly. + expect(result, contains('requireEl.src = "require.js";')); + // stack trace mapper source is interpolated correctly. + expect(result, contains('mapperEl.src = "mapper.js";')); + // data-main is set to correct bootstrap module. + expect(result, contains('requireEl.setAttribute("data-main", "main_module");')); + // bootstrap main module has correct imports. + expect(result, contains('require(["foo/bar/main.js", "dart_sdk"],' + ' function(app, dart_sdk) {')); + }); + + test('generateMainModule embeds urls correctly', () { + final String result = generateMainModule( + entrypoint: 'foo/bar/main.js', + ); + // bootstrap main module has correct defined module. + expect(result, contains('define("main_module", ["foo/bar/main.js", "dart_sdk"], ' + 'function(app, dart_sdk) {')); + }); +}