Unverified Commit 3c30e3cb authored by Jackson Gardner's avatar Jackson Gardner Committed by GitHub

Flutter Web Bootstrapping Improvements (#144434)

This makes several changes to flutter web app bootstrapping.
* The build now produces a `flutter_bootstrap.js` file.
  * By default, this file does the basic streamlined startup of a flutter app with the service worker settings and no user configuration.
  * The user can also put a `flutter_bootstrap.js` file in the `web` subdirectory in the project directory which can have whatever custom bootstrapping logic they'd like to write instead. This file is also templated, and can use any of the tokens  that can be used with the `index.html` (with the exception of `{{flutter_bootstrap_js}}`, see below).
* Introduced a few new templating tokens for `index.html`:
  * `{{flutter_js}}` => inlines the entirety of `flutter.js`
  * `{{flutter_service_worker_version}}` => replaced directly by the service worker version. This can be used instead of the script that sets the `serviceWorkerVersion` local variable that we used to have by default.
  * `{{flutter_bootstrap_js}}` => inlines the entirety of `flutter_bootstrap.js` (this token obviously doesn't apply to `flutter_bootstrap.js` itself).
* Changed `IndexHtml` to be called `WebTemplate` instead, since it is used for more than just the index.html now.
* We now emit warnings at build time for certain deprecated flows:
  * Warn on the old service worker version pattern (i.e.`(const|var) serviceWorkerVersion = null`) and recommends using `{{flutter_service_worker_version}}` token instead
  * Warn on use of `FlutterLoader.loadEntrypoint` and recommend using `FlutterLoader.load` instead
  * Warn on manual loading of `flutter_service_worker.js`.
* The default `index.html` on `flutter create` now uses an async script tag with `flutter_bootstrap.js`.
parent 5b006bf5
...@@ -41,23 +41,8 @@ found in the LICENSE file. --> ...@@ -41,23 +41,8 @@ found in the LICENSE file. -->
const serviceWorkerVersion = null; const serviceWorkerVersion = null;
</script> </script>
<!-- This script adds the flutter initialization JS code --> <!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head> </head>
<body> <body>
<script> <script src="flutter_bootstrap.js" async></script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
onEntrypointLoaded: function(engineInitializer) {
engineInitializer.initializeEngine().then(function(appRunner) {
appRunner.runApp();
});
}
});
});
</script>
</body> </body>
</html> </html>
{{flutter_js}}
{{flutter_build_config}}
_flutter.loader.load({
config: {
// Use the local CanvasKit bundle instead of the CDN to reduce test flakiness.
canvasKitBaseUrl: "/canvaskit/",
},
});
\ No newline at end of file
...@@ -6,16 +6,8 @@ found in the LICENSE file. --> ...@@ -6,16 +6,8 @@ found in the LICENSE file. -->
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Web Benchmarks</title> <title>Web Benchmarks</title>
<script src="flutter.js"></script>
</head> </head>
<body> <body>
<script> <script src="flutter_bootstrap.js" async></script>
{{flutter_build_config}}
_flutter.loader.load({
config: {
canvasKitBaseUrl: '/canvaskit/',
}
});
</script>
</body> </body>
</html> </html>
...@@ -163,7 +163,11 @@ Future<void> _waitForAppToLoad( ...@@ -163,7 +163,11 @@ Future<void> _waitForAppToLoad(
print('Waiting for app to load $waitForCounts'); print('Waiting for app to load $waitForCounts');
await Future.any(<Future<Object?>>[ await Future.any(<Future<Object?>>[
() async { () async {
int tries = 1;
while (!waitForCounts.entries.every((MapEntry<String, int> entry) => (requestedPathCounts[entry.key] ?? 0) >= entry.value)) { while (!waitForCounts.entries.every((MapEntry<String, int> entry) => (requestedPathCounts[entry.key] ?? 0) >= entry.value)) {
if (tries++ % 20 == 0) {
print('Still waiting. Requested so far: $requestedPathCounts');
}
await Future<void>.delayed(const Duration(milliseconds: 100)); await Future<void>.delayed(const Duration(milliseconds: 100));
} }
}(), }(),
...@@ -304,15 +308,16 @@ Future<void> runWebServiceWorkerTest({ ...@@ -304,15 +308,16 @@ Future<void> runWebServiceWorkerTest({
'flutter.js': 1, 'flutter.js': 1,
'main.dart.js': 1, 'main.dart.js': 1,
'flutter_service_worker.js': 1, 'flutter_service_worker.js': 1,
'flutter_bootstrap.js': 1,
'assets/FontManifest.json': 1, 'assets/FontManifest.json': 1,
'assets/AssetManifest.bin.json': 1, 'assets/AssetManifest.bin.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1, 'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1, 'CLOSE': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'. // In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless) if (!headless)
...<String, int>{ ...<String, int>{
'manifest.json': 1, 'manifest.json': 1,
'favicon.ico': 1, 'favicon.png': 1,
}, },
}); });
expect(reportedVersion, '1'); expect(reportedVersion, '1');
...@@ -346,12 +351,13 @@ Future<void> runWebServiceWorkerTest({ ...@@ -346,12 +351,13 @@ Future<void> runWebServiceWorkerTest({
if (shouldExpectFlutterJs) if (shouldExpectFlutterJs)
'flutter.js': 1, 'flutter.js': 1,
'flutter_service_worker.js': 2, 'flutter_service_worker.js': 2,
'flutter_bootstrap.js': 1,
'main.dart.js': 1, 'main.dart.js': 1,
'assets/AssetManifest.bin.json': 1, 'assets/AssetManifest.bin.json': 1,
'assets/FontManifest.json': 1, 'assets/FontManifest.json': 1,
'CLOSE': 1, 'CLOSE': 1,
if (!headless) if (!headless)
'favicon.ico': 1, 'favicon.png': 1,
}); });
expect(reportedVersion, '2'); expect(reportedVersion, '2');
...@@ -377,14 +383,15 @@ Future<void> runWebServiceWorkerTest({ ...@@ -377,14 +383,15 @@ Future<void> runWebServiceWorkerTest({
'main.dart.js': 1, 'main.dart.js': 1,
'assets/FontManifest.json': 1, 'assets/FontManifest.json': 1,
'flutter_service_worker.js': 1, 'flutter_service_worker.js': 1,
'flutter_bootstrap.js': 1,
'assets/AssetManifest.bin.json': 1, 'assets/AssetManifest.bin.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1, 'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1, 'CLOSE': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'. // In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless) if (!headless)
...<String, int>{ ...<String, int>{
'manifest.json': 1, 'manifest.json': 1,
'favicon.ico': 1, 'favicon.png': 1,
}, },
}); });
...@@ -429,6 +436,7 @@ Future<void> runWebServiceWorkerTest({ ...@@ -429,6 +436,7 @@ Future<void> runWebServiceWorkerTest({
if (shouldExpectFlutterJs) if (shouldExpectFlutterJs)
'flutter.js': 1, 'flutter.js': 1,
'flutter_service_worker.js': 2, 'flutter_service_worker.js': 2,
'flutter_bootstrap.js': 1,
'main.dart.js': 1, 'main.dart.js': 1,
'assets/AssetManifest.bin.json': 1, 'assets/AssetManifest.bin.json': 1,
'assets/FontManifest.json': 1, 'assets/FontManifest.json': 1,
...@@ -436,7 +444,7 @@ Future<void> runWebServiceWorkerTest({ ...@@ -436,7 +444,7 @@ Future<void> runWebServiceWorkerTest({
if (!headless) if (!headless)
...<String, int>{ ...<String, int>{
'manifest.json': 1, 'manifest.json': 1,
'favicon.ico': 1, 'favicon.png': 1,
}, },
}); });
...@@ -508,8 +516,8 @@ Future<void> runWebServiceWorkerTestWithCachingResources({ ...@@ -508,8 +516,8 @@ Future<void> runWebServiceWorkerTestWithCachingResources({
workingDirectory: _testAppWebDirectory, workingDirectory: _testAppWebDirectory,
); );
final bool shouldExpectFlutterJs = testType != ServiceWorkerTestType.withoutFlutterJs; final bool usesFlutterBootstrapJs = testType == ServiceWorkerTestType.generatedEntrypoint;
final bool shouldExpectFlutterJs = !usesFlutterBootstrapJs && testType != ServiceWorkerTestType.withoutFlutterJs;
print('BEGIN runWebServiceWorkerTestWithCachingResources(headless: $headless, testType: $testType)'); print('BEGIN runWebServiceWorkerTestWithCachingResources(headless: $headless, testType: $testType)');
try { try {
...@@ -534,14 +542,15 @@ Future<void> runWebServiceWorkerTestWithCachingResources({ ...@@ -534,14 +542,15 @@ Future<void> runWebServiceWorkerTestWithCachingResources({
'flutter.js': 1, 'flutter.js': 1,
'main.dart.js': 1, 'main.dart.js': 1,
'flutter_service_worker.js': 1, 'flutter_service_worker.js': 1,
'flutter_bootstrap.js': usesFlutterBootstrapJs ? 2 : 1,
'assets/FontManifest.json': 1, 'assets/FontManifest.json': 1,
'assets/AssetManifest.bin.json': 1, 'assets/AssetManifest.bin.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1, 'assets/fonts/MaterialIcons-Regular.otf': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'. // In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless) if (!headless)
...<String, int>{ ...<String, int>{
'manifest.json': 1, 'manifest.json': 1,
'favicon.ico': 1, 'favicon.png': 1,
}, },
}); });
...@@ -593,13 +602,14 @@ Future<void> runWebServiceWorkerTestWithCachingResources({ ...@@ -593,13 +602,14 @@ Future<void> runWebServiceWorkerTestWithCachingResources({
'flutter.js': 1, 'flutter.js': 1,
'main.dart.js': 1, 'main.dart.js': 1,
'flutter_service_worker.js': 2, 'flutter_service_worker.js': 2,
'flutter_bootstrap.js': usesFlutterBootstrapJs ? 2 : 1,
'assets/FontManifest.json': 1, 'assets/FontManifest.json': 1,
'assets/AssetManifest.bin.json': 1, 'assets/AssetManifest.bin.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1, 'assets/fonts/MaterialIcons-Regular.otf': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'. // In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless) if (!headless)
...<String, int>{ ...<String, int>{
'favicon.ico': 1, 'favicon.png': 1,
}, },
}); });
} finally { } finally {
...@@ -682,11 +692,11 @@ Future<void> runWebServiceWorkerTestWithBlockedServiceWorkers({ ...@@ -682,11 +692,11 @@ Future<void> runWebServiceWorkerTestWithBlockedServiceWorkers({
'assets/FontManifest.json': 1, 'assets/FontManifest.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1, 'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1, 'CLOSE': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'. // In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless) if (!headless)
...<String, int>{ ...<String, int>{
'manifest.json': 1, 'manifest.json': 1,
'favicon.ico': 1, 'favicon.png': 1,
}, },
}); });
} finally { } finally {
...@@ -770,14 +780,15 @@ Future<void> runWebServiceWorkerTestWithCustomServiceWorkerVersion({ ...@@ -770,14 +780,15 @@ Future<void> runWebServiceWorkerTestWithCustomServiceWorkerVersion({
'main.dart.js': 1, 'main.dart.js': 1,
'CLOSE': 1, 'CLOSE': 1,
'flutter_service_worker.js': 1, 'flutter_service_worker.js': 1,
'flutter_bootstrap.js': 1,
'assets/FontManifest.json': 1, 'assets/FontManifest.json': 1,
'assets/AssetManifest.bin.json': 1, 'assets/AssetManifest.bin.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1, 'assets/fonts/MaterialIcons-Regular.otf': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'. // In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless) if (!headless)
...<String, int>{ ...<String, int>{
'manifest.json': 1, 'manifest.json': 1,
'favicon.ico': 1, 'favicon.png': 1,
}, },
}); });
...@@ -794,11 +805,11 @@ Future<void> runWebServiceWorkerTestWithCustomServiceWorkerVersion({ ...@@ -794,11 +805,11 @@ Future<void> runWebServiceWorkerTestWithCustomServiceWorkerVersion({
'assets/FontManifest.json': 1, 'assets/FontManifest.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1, 'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1, 'CLOSE': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'. // In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless) if (!headless)
...<String, int>{ ...<String, int>{
'manifest.json': 1, 'manifest.json': 1,
'favicon.ico': 1, 'favicon.png': 1,
}, },
}); });
...@@ -816,11 +827,11 @@ Future<void> runWebServiceWorkerTestWithCustomServiceWorkerVersion({ ...@@ -816,11 +827,11 @@ Future<void> runWebServiceWorkerTestWithCustomServiceWorkerVersion({
'assets/FontManifest.json': 1, 'assets/FontManifest.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1, 'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1, 'CLOSE': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'. // In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless) if (!headless)
...<String, int>{ ...<String, int>{
'manifest.json': 1, 'manifest.json': 1,
'favicon.ico': 1, 'favicon.png': 1,
}, },
}); });
} finally { } finally {
......
...@@ -5,14 +5,16 @@ ...@@ -5,14 +5,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
Future<void> main() async { Future<void> main() async {
runApp(const Scaffold( runApp(const Directionality(
body: Center( textDirection: TextDirection.ltr,
child: Column( child: Scaffold(
children: <Widget>[ body: Center(
Icon(Icons.ac_unit), child: Column(
Text('Hello, World', textDirection: TextDirection.ltr), children: <Widget>[
], Icon(Icons.ac_unit),
Text('Hello, World', textDirection: TextDirection.ltr),
],
),
), ),
), )));
));
} }
...@@ -12,6 +12,9 @@ found in the LICENSE file. --> ...@@ -12,6 +12,9 @@ found in the LICENSE file. -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test"> <meta name="apple-mobile-web-app-title" content="Web Test">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
</head> </head>
<body> <body>
......
...@@ -12,6 +12,9 @@ found in the LICENSE file. --> ...@@ -12,6 +12,9 @@ found in the LICENSE file. -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test"> <meta name="apple-mobile-web-app-title" content="Web Test">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script> <script>
......
...@@ -12,6 +12,9 @@ found in the LICENSE file. --> ...@@ -12,6 +12,9 @@ found in the LICENSE file. -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test"> <meta name="apple-mobile-web-app-title" content="Web Test">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script> <script>
// The value below is injected by flutter build, do not touch. // The value below is injected by flutter build, do not touch.
......
...@@ -13,6 +13,9 @@ found in the LICENSE file. --> ...@@ -13,6 +13,9 @@ found in the LICENSE file. -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test"> <meta name="apple-mobile-web-app-title" content="Web Test">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script> <script>
// The value below is injected by flutter build, do not touch. // The value below is injected by flutter build, do not touch.
......
...@@ -16,6 +16,9 @@ found in the LICENSE file. --> ...@@ -16,6 +16,9 @@ found in the LICENSE file. -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test"> <meta name="apple-mobile-web-app-title" content="Web Test">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script nonce="SOME_NONCE"> <script nonce="SOME_NONCE">
// The value below is injected by flutter build, do not touch. // The value below is injected by flutter build, do not touch.
......
...@@ -16,6 +16,9 @@ found in the LICENSE file. --> ...@@ -16,6 +16,9 @@ found in the LICENSE file. -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test"> <meta name="apple-mobile-web-app-title" content="Web Test">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script> <script>
// The value below is injected by flutter build, do not touch. // The value below is injected by flutter build, do not touch.
......
...@@ -12,6 +12,9 @@ found in the LICENSE file. --> ...@@ -12,6 +12,9 @@ found in the LICENSE file. -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test"> <meta name="apple-mobile-web-app-title" content="Web Test">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script> <script>
// The value below is injected by flutter build, do not touch. // The value below is injected by flutter build, do not touch.
......
...@@ -12,6 +12,9 @@ found in the LICENSE file. --> ...@@ -12,6 +12,9 @@ found in the LICENSE file. -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test"> <meta name="apple-mobile-web-app-title" content="Web Test">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script> <script>
// The value below is injected by flutter build, do not touch. // The value below is injected by flutter build, do not touch.
......
...@@ -12,6 +12,9 @@ found in the LICENSE file. --> ...@@ -12,6 +12,9 @@ found in the LICENSE file. -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test"> <meta name="apple-mobile-web-app-title" content="Web Test">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
</head> </head>
<body> <body>
......
{{flutter_build_config}}
{{flutter_js}}
_flutter.loader.load({
config: {
// Use the local CanvasKit bundle instead of the CDN to reduce test flakiness.
canvasKitBaseUrl: "/canvaskit/",
},
});
\ No newline at end of file
...@@ -5,17 +5,8 @@ found in the LICENSE file. --> ...@@ -5,17 +5,8 @@ found in the LICENSE file. -->
<html> <html>
<head> <head>
<title>Web Integration Tests</title> <title>Web Integration Tests</title>
<script src="flutter.js"></script>
</head> </head>
<body> <body>
<script> <script src="flutter_bootstrap.js" async></script>
{{flutter_build_config}}
_flutter.loader.load({
config: {
// Use the local CanvasKit bundle instead of the CDN to reduce test flakiness.
canvasKitBaseUrl: "/canvaskit/",
},
});
</script>
</body> </body>
</html> </html>
...@@ -17,11 +17,12 @@ import '../../dart/language_version.dart'; ...@@ -17,11 +17,12 @@ import '../../dart/language_version.dart';
import '../../dart/package_map.dart'; import '../../dart/package_map.dart';
import '../../flutter_plugins.dart'; import '../../flutter_plugins.dart';
import '../../globals.dart' as globals; import '../../globals.dart' as globals;
import '../../html_utils.dart';
import '../../project.dart'; import '../../project.dart';
import '../../web/bootstrap.dart';
import '../../web/compile.dart'; import '../../web/compile.dart';
import '../../web/file_generators/flutter_service_worker_js.dart'; import '../../web/file_generators/flutter_service_worker_js.dart';
import '../../web/file_generators/main_dart.dart' as main_dart; import '../../web/file_generators/main_dart.dart' as main_dart;
import '../../web_template.dart';
import '../build_system.dart'; import '../build_system.dart';
import '../depfile.dart'; import '../depfile.dart';
import '../exceptions.dart'; import '../exceptions.dart';
...@@ -328,18 +329,37 @@ class Dart2WasmTarget extends Dart2WebTarget { ...@@ -328,18 +329,37 @@ class Dart2WasmTarget extends Dart2WebTarget {
/// Unpacks the dart2js or dart2wasm compilation and resources to a given /// Unpacks the dart2js or dart2wasm compilation and resources to a given
/// output directory. /// output directory.
class WebReleaseBundle extends Target { class WebReleaseBundle extends Target {
WebReleaseBundle(List<WebCompilerConfig> configs) : this._withTargets( WebReleaseBundle(List<WebCompilerConfig> configs) : this._(
configs.map((WebCompilerConfig config) => compileTargets: configs.map((WebCompilerConfig config) =>
switch (config) { switch (config) {
WasmCompilerConfig() => Dart2WasmTarget(config), WasmCompilerConfig() => Dart2WasmTarget(config),
JsCompilerConfig() => Dart2JSTarget(config), JsCompilerConfig() => Dart2JSTarget(config),
} }
).toList() ).toList(),
); );
const WebReleaseBundle._withTargets(this.compileTargets); WebReleaseBundle._({
required this.compileTargets,
}) : templatedFilesTarget = WebTemplatedFiles(generateBuildConfigString(compileTargets));
static String generateBuildConfigString(List<Dart2WebTarget> compileTargets) {
final List<Map<String, Object?>> buildDescriptions = compileTargets.map(
(Dart2WebTarget target) => target.buildConfig
).toList();
final Map<String, Object?> buildConfig = <String, Object?>{
'engineRevision': globals.flutterVersion.engineRevision,
'builds': buildDescriptions,
};
return '''
if (!window._flutter) {
window._flutter = {};
}
_flutter.buildConfig = ${jsonEncode(buildConfig)};
''';
}
final List<Dart2WebTarget> compileTargets; final List<Dart2WebTarget> compileTargets;
final WebTemplatedFiles templatedFilesTarget;
List<String> get buildFiles => compileTargets.fold( List<String> get buildFiles => compileTargets.fold(
const Iterable<String>.empty(), const Iterable<String>.empty(),
...@@ -350,7 +370,10 @@ class WebReleaseBundle extends Target { ...@@ -350,7 +370,10 @@ class WebReleaseBundle extends Target {
String get name => 'web_release_bundle'; String get name => 'web_release_bundle';
@override @override
List<Target> get dependencies => compileTargets; List<Target> get dependencies => <Target>[
...compileTargets,
templatedFilesTarget,
];
@override @override
List<Source> get inputs => <Source>[ List<Source> get inputs => <Source>[
...@@ -371,11 +394,12 @@ class WebReleaseBundle extends Target { ...@@ -371,11 +394,12 @@ class WebReleaseBundle extends Target {
@override @override
Future<void> build(Environment environment) async { Future<void> build(Environment environment) async {
final FileSystem fileSystem = environment.fileSystem;
for (final File outputFile in environment.buildDir.listSync(recursive: true).whereType<File>()) { for (final File outputFile in environment.buildDir.listSync(recursive: true).whereType<File>()) {
final String basename = environment.fileSystem.path.basename(outputFile.path); final String basename = fileSystem.path.basename(outputFile.path);
if (buildFiles.contains(basename)) { if (buildFiles.contains(basename)) {
outputFile.copySync( outputFile.copySync(
environment.outputDir.childFile(environment.fileSystem.path.basename(outputFile.path)).path environment.outputDir.childFile(fileSystem.path.basename(outputFile.path)).path
); );
} }
} }
...@@ -404,34 +428,18 @@ class WebReleaseBundle extends Target { ...@@ -404,34 +428,18 @@ class WebReleaseBundle extends Target {
// Copy other resource files out of web/ directory. // Copy other resource files out of web/ directory.
final List<File> outputResourcesFiles = <File>[]; final List<File> outputResourcesFiles = <File>[];
for (final File inputFile in inputResourceFiles) { for (final File inputFile in inputResourceFiles) {
final File outputFile = environment.fileSystem.file(environment.fileSystem.path.join( final String relativePath = fileSystem.path.relative(inputFile.path, from: webResources.path);
if (relativePath == 'index.html' || relativePath == 'flutter_bootstrap.js') {
// Skip these, these are handled by the templated file target.
continue;
}
final File outputFile = fileSystem.file(fileSystem.path.join(
environment.outputDir.path, environment.outputDir.path,
environment.fileSystem.path.relative(inputFile.path, from: webResources.path))); relativePath));
if (!outputFile.parent.existsSync()) { if (!outputFile.parent.existsSync()) {
outputFile.parent.createSync(recursive: true); outputFile.parent.createSync(recursive: true);
} }
outputResourcesFiles.add(outputFile); outputResourcesFiles.add(outputFile);
// insert a random hash into the requests for service_worker.js. This is not a content hash,
// because it would need to be the hash for the entire bundle and not just the resource
// in question.
if (environment.fileSystem.path.basename(inputFile.path) == 'index.html') {
final List<Map<String, Object?>> buildDescriptions = compileTargets.map(
(Dart2WebTarget target) => target.buildConfig
).toList();
final Map<String, Object?> buildConfig = <String, Object?>{
'engineRevision': globals.flutterVersion.engineRevision,
'builds': buildDescriptions,
};
final String buildConfigString = '_flutter.buildConfig = ${jsonEncode(buildConfig)};';
final IndexHtml indexHtml = IndexHtml(inputFile.readAsStringSync());
indexHtml.applySubstitutions(
baseHref: environment.defines[kBaseHref] ?? '/',
serviceWorkerVersion: Random().nextInt(4294967296).toString(),
buildConfig: buildConfigString,
);
outputFile.writeAsStringSync(indexHtml.content);
continue;
}
inputFile.copySync(outputFile.path); inputFile.copySync(outputFile.path);
} }
final Depfile resourceFile = Depfile(inputResourceFiles, outputResourcesFiles); final Depfile resourceFile = Depfile(inputResourceFiles, outputResourcesFiles);
...@@ -461,6 +469,110 @@ class WebReleaseBundle extends Target { ...@@ -461,6 +469,110 @@ class WebReleaseBundle extends Target {
} }
} }
class WebTemplatedFiles extends Target {
WebTemplatedFiles(this.buildConfigString);
final String buildConfigString;
@override
String get buildKey => buildConfigString;
void _emitWebTemplateWarning(
Environment environment,
String filePath,
WebTemplateWarning warning
) {
environment.logger.printWarning(
'Warning: In $filePath:${warning.lineNumber}: ${warning.warningText}'
);
}
@override
Future<void> build(Environment environment) async {
final Directory webResources = environment.projectDir
.childDirectory('web');
final File inputFlutterBootstrapJs = webResources.childFile('flutter_bootstrap.js');
final String inputBootstrapContent;
if (await inputFlutterBootstrapJs.exists()) {
inputBootstrapContent = await inputFlutterBootstrapJs.readAsString();
} else {
inputBootstrapContent = generateDefaultFlutterBootstrapScript();
}
final WebTemplate bootstrapTemplate = WebTemplate(inputBootstrapContent);
for (final WebTemplateWarning warning in bootstrapTemplate.getWarnings()) {
_emitWebTemplateWarning(environment, 'flutter_bootstrap.js', warning);
}
final FileSystem fileSystem = environment.fileSystem;
final File flutterJsFile = fileSystem.file(fileSystem.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
));
// Insert a random hash into the requests for service_worker.js. This is not a content hash,
// because it would need to be the hash for the entire bundle and not just the resource
// in question.
final String serviceWorkerVersion = Random().nextInt(4294967296).toString();
bootstrapTemplate.applySubstitutions(
baseHref: '',
serviceWorkerVersion: serviceWorkerVersion,
flutterJsFile: flutterJsFile,
buildConfig: buildConfigString,
);
final File outputFlutterBootstrapJs = fileSystem.file(fileSystem.path.join(
environment.outputDir.path,
'flutter_bootstrap.js'
));
await outputFlutterBootstrapJs.writeAsString(bootstrapTemplate.content);
await for (final FileSystemEntity file in webResources.list(recursive: true)) {
if (file is File && file.basename == 'index.html') {
final WebTemplate indexHtmlTemplate = WebTemplate(file.readAsStringSync());
final String relativePath = fileSystem.path.relative(file.path, from: webResources.path);
for (final WebTemplateWarning warning in indexHtmlTemplate.getWarnings()) {
_emitWebTemplateWarning(environment, relativePath, warning);
}
indexHtmlTemplate.applySubstitutions(
baseHref: environment.defines[kBaseHref] ?? '/',
serviceWorkerVersion: serviceWorkerVersion,
flutterJsFile: flutterJsFile,
buildConfig: buildConfigString,
flutterBootstrapJs: bootstrapTemplate.content,
);
final File outputIndexHtml = fileSystem.file(fileSystem.path.join(
environment.outputDir.path,
relativePath,
));
await outputIndexHtml.create(recursive: true);
await outputIndexHtml.writeAsString(indexHtmlTemplate.content);
}
}
}
@override
List<Target> get dependencies => <Target>[];
@override
List<Source> get inputs => const <Source>[
Source.pattern('{PROJECT_DIR}/web/*/index.html'),
Source.pattern('{PROJECT_DIR}/web/flutter_bootstrap.js'),
Source.hostArtifact(HostArtifact.flutterWebSdk),
];
@override
String get name => 'web_templated_files';
@override
List<Source> get outputs => const <Source>[
Source.pattern('{OUTPUT_DIR}/*/index.html'),
Source.pattern('{OUTPUT_DIR}/flutter_bootstrap.js'),
];
}
/// Static assets provided by the Flutter SDK that do not change, such as /// Static assets provided by the Flutter SDK that do not change, such as
/// CanvasKit. /// CanvasKit.
/// ///
...@@ -596,6 +708,7 @@ class WebServiceWorker extends Target { ...@@ -596,6 +708,7 @@ class WebServiceWorker extends Target {
'main.dart.mjs', 'main.dart.mjs',
], ],
'index.html', 'index.html',
'flutter_bootstrap.js',
if (urlToHash.containsKey('assets/AssetManifest.bin.json')) if (urlToHash.containsKey('assets/AssetManifest.bin.json'))
'assets/AssetManifest.bin.json', 'assets/AssetManifest.bin.json',
if (urlToHash.containsKey('assets/FontManifest.json')) if (urlToHash.containsKey('assets/FontManifest.json'))
......
...@@ -8,13 +8,13 @@ import '../base/utils.dart'; ...@@ -8,13 +8,13 @@ import '../base/utils.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../features.dart'; import '../features.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../html_utils.dart';
import '../project.dart'; import '../project.dart';
import '../runner/flutter_command.dart' import '../runner/flutter_command.dart'
show DevelopmentArtifact, FlutterCommandResult, FlutterOptions; show DevelopmentArtifact, FlutterCommandResult, FlutterOptions;
import '../web/compile.dart'; import '../web/compile.dart';
import '../web/file_generators/flutter_service_worker_js.dart'; import '../web/file_generators/flutter_service_worker_js.dart';
import '../web/web_constants.dart'; import '../web/web_constants.dart';
import '../web_template.dart';
import 'build.dart'; import 'build.dart';
class BuildWebCommand extends BuildSubCommand { class BuildWebCommand extends BuildSubCommand {
......
...@@ -34,13 +34,13 @@ import '../dart/package_map.dart'; ...@@ -34,13 +34,13 @@ import '../dart/package_map.dart';
import '../devfs.dart'; import '../devfs.dart';
import '../device.dart'; import '../device.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../html_utils.dart';
import '../project.dart'; import '../project.dart';
import '../vmservice.dart'; import '../vmservice.dart';
import '../web/bootstrap.dart'; import '../web/bootstrap.dart';
import '../web/chrome.dart'; import '../web/chrome.dart';
import '../web/compile.dart'; import '../web/compile.dart';
import '../web/memory_fs.dart'; import '../web/memory_fs.dart';
import '../web_template.dart';
typedef DwdsLauncher = Future<Dwds> Function({ typedef DwdsLauncher = Future<Dwds> Function({
required AssetReader assetReader, required AssetReader assetReader,
...@@ -120,7 +120,7 @@ class WebAssetServer implements AssetReader { ...@@ -120,7 +120,7 @@ class WebAssetServer implements AssetReader {
this._nullSafetyMode, this._nullSafetyMode,
this._ddcModuleSystem, { this._ddcModuleSystem, {
required this.webRenderer, required this.webRenderer,
}) : basePath = _getIndexHtml().getBaseHref(); }) : basePath = _getWebTemplate('index.html', _kDefaultIndex).getBaseHref();
// Fallback to "application/octet-stream" on null which // Fallback to "application/octet-stream" on null which
// makes no claims as to the structure of the data. // makes no claims as to the structure of the data.
...@@ -386,7 +386,11 @@ class WebAssetServer implements AssetReader { ...@@ -386,7 +386,11 @@ class WebAssetServer implements AssetReader {
// If the response is `/`, then we are requesting the index file. // If the response is `/`, then we are requesting the index file.
if (requestPath == '/' || requestPath.isEmpty) { if (requestPath == '/' || requestPath.isEmpty) {
return _serveIndex(); return _serveIndexHtml();
}
if (requestPath == 'flutter_bootstrap.js') {
return _serveFlutterBootstrapJs();
} }
final Map<String, String> headers = <String, String>{}; final Map<String, String> headers = <String, String>{};
...@@ -478,7 +482,7 @@ class WebAssetServer implements AssetReader { ...@@ -478,7 +482,7 @@ class WebAssetServer implements AssetReader {
requestPath.startsWith('canvaskit/')) { requestPath.startsWith('canvaskit/')) {
return shelf.Response.notFound(''); return shelf.Response.notFound('');
} }
return _serveIndex(); return _serveIndexHtml();
} }
// For real files, use a serialized file stat plus path as a revision. // For real files, use a serialized file stat plus path as a revision.
...@@ -524,8 +528,7 @@ class WebAssetServer implements AssetReader { ...@@ -524,8 +528,7 @@ class WebAssetServer implements AssetReader {
/// Determines what rendering backed to use. /// Determines what rendering backed to use.
final WebRendererMode webRenderer; final WebRendererMode webRenderer;
shelf.Response _serveIndex() { String get _buildConfigString {
final IndexHtml indexHtml = _getIndexHtml();
final Map<String, dynamic> buildConfig = <String, dynamic>{ final Map<String, dynamic> buildConfig = <String, dynamic>{
'engineRevision': globals.flutterVersion.engineRevision, 'engineRevision': globals.flutterVersion.engineRevision,
'builds': <dynamic>[ 'builds': <dynamic>[
...@@ -536,19 +539,52 @@ class WebAssetServer implements AssetReader { ...@@ -536,19 +539,52 @@ class WebAssetServer implements AssetReader {
}, },
], ],
}; };
final String buildConfigString = '_flutter.buildConfig = ${jsonEncode(buildConfig)};'; return '''
if (!window._flutter) {
window._flutter = {};
}
_flutter.buildConfig = ${jsonEncode(buildConfig)};
''';
}
File get _flutterJsFile => globals.fs.file(globals.fs.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
));
String get _flutterBootstrapJsContent {
final WebTemplate bootstrapTemplate = _getWebTemplate(
'flutter_bootstrap.js',
generateDefaultFlutterBootstrapScript()
);
bootstrapTemplate.applySubstitutions(
baseHref: '/',
serviceWorkerVersion: null,
buildConfig: _buildConfigString,
flutterJsFile: _flutterJsFile,
);
return bootstrapTemplate.content;
}
shelf.Response _serveFlutterBootstrapJs() {
return shelf.Response.ok(_flutterBootstrapJsContent, headers: <String, String>{
HttpHeaders.contentTypeHeader: 'text/javascript',
});
}
shelf.Response _serveIndexHtml() {
final WebTemplate indexHtml = _getWebTemplate('index.html', _kDefaultIndex);
indexHtml.applySubstitutions( indexHtml.applySubstitutions(
// Currently, we don't support --base-href for the "run" command. // Currently, we don't support --base-href for the "run" command.
baseHref: '/', baseHref: '/',
serviceWorkerVersion: null, serviceWorkerVersion: null,
buildConfig: buildConfigString, buildConfig: _buildConfigString,
flutterJsFile: _flutterJsFile,
flutterBootstrapJs: _flutterBootstrapJsContent,
); );
return shelf.Response.ok(indexHtml.content, headers: <String, String>{
final Map<String, String> headers = <String, String>{
HttpHeaders.contentTypeHeader: 'text/html', HttpHeaders.contentTypeHeader: 'text/html',
}; });
return shelf.Response.ok(indexHtml.content, headers: headers);
} }
// Attempt to resolve `path` to a dart file. // Attempt to resolve `path` to a dart file.
...@@ -860,6 +896,21 @@ class WebDevFS implements DevFS { ...@@ -860,6 +896,21 @@ class WebDevFS implements DevFS {
@override @override
final Directory rootDirectory; final Directory rootDirectory;
Future<void> _validateTemplateFile(String filename) async {
final File file =
globals.fs.currentDirectory.childDirectory('web').childFile(filename);
if (!await file.exists()) {
return;
}
final WebTemplate template = WebTemplate(await file.readAsString());
for (final WebTemplateWarning warning in template.getWarnings()) {
globals.logger.printWarning(
'Warning: In $filename:${warning.lineNumber}: ${warning.warningText}'
);
}
}
@override @override
Future<UpdateFSReport> update({ Future<UpdateFSReport> update({
required Uri mainUri, required Uri mainUri,
...@@ -950,6 +1001,8 @@ class WebDevFS implements DevFS { ...@@ -950,6 +1001,8 @@ class WebDevFS implements DevFS {
); );
} }
} }
await _validateTemplateFile('index.html');
await _validateTemplateFile('flutter_bootstrap.js');
final DateTime candidateCompileTime = DateTime.now(); final DateTime candidateCompileTime = DateTime.now();
if (fullRestart) { if (fullRestart) {
generator.reset(); generator.reset();
...@@ -1173,10 +1226,10 @@ String? _stripBasePath(String path, String basePath) { ...@@ -1173,10 +1226,10 @@ String? _stripBasePath(String path, String basePath) {
return stripLeadingSlash(path); return stripLeadingSlash(path);
} }
IndexHtml _getIndexHtml() { WebTemplate _getWebTemplate(String filename, String fallbackContent) {
final File indexHtml = final File template =
globals.fs.currentDirectory.childDirectory('web').childFile('index.html'); globals.fs.currentDirectory.childDirectory('web').childFile(filename);
final String htmlContent = final String htmlContent =
indexHtml.existsSync() ? indexHtml.readAsStringSync() : _kDefaultIndex; template.existsSync() ? template.readAsStringSync() : fallbackContent;
return IndexHtml(htmlContent); return WebTemplate(htmlContent);
} }
...@@ -472,3 +472,16 @@ String generateTestBootstrapFileContents( ...@@ -472,3 +472,16 @@ String generateTestBootstrapFileContents(
})(); })();
'''; ''';
} }
String generateDefaultFlutterBootstrapScript() {
return '''
{{flutter_js}}
{{flutter_build_config}}
_flutter.loader.load({
serviceWorkerSettings: {
serviceWorkerVersion: {{flutter_service_worker_version}}
}
});
''';
}
// Copyright 2014 The Flutter 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 '../web_constants.dart';
String generateWasmBootstrapFile(bool isSkwasm) {
return '''
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(async function () {
let dart2wasm_runtime;
let moduleInstance;
try {
const dartModulePromise = WebAssembly.compileStreaming(fetch('main.dart.wasm'));
${generateImports(isSkwasm)}
dart2wasm_runtime = await import('./main.dart.mjs');
moduleInstance = await dart2wasm_runtime.instantiate(dartModulePromise, imports);
} catch (exception) {
console.error(`Failed to fetch and instantiate wasm module: \${exception}`);
console.error('$kWasmMoreInfo');
}
if (moduleInstance) {
try {
await dart2wasm_runtime.invoke(moduleInstance);
} catch (exception) {
console.error(`Exception while invoking test: \${exception}`);
}
}
})();
''';
}
String generateImports(bool isSkwasm) {
if (isSkwasm) {
return r'''
const imports = new Promise((resolve, reject) => {
const skwasmScript = document.createElement('script');
skwasmScript.src = 'canvaskit/skwasm.js';
document.body.appendChild(skwasmScript);
skwasmScript.addEventListener('load', async () => {
const skwasmInstance = await skwasm();
window._flutter_skwasmInstance = skwasmInstance;
resolve({
'skwasm': skwasmInstance.wasmExports,
'skwasmWrapper': skwasmInstance,
'ffi': {
'memory': skwasmInstance.wasmMemory,
}
});
});
});
''';
} else {
return ' const imports = {};';
}
}
...@@ -6,10 +6,20 @@ import 'package:html/dom.dart'; ...@@ -6,10 +6,20 @@ import 'package:html/dom.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'base/common.dart'; import 'base/common.dart';
import 'base/file_system.dart';
/// Placeholder for base href /// Placeholder for base href
const String kBaseHrefPlaceholder = r'$FLUTTER_BASE_HREF'; const String kBaseHrefPlaceholder = r'$FLUTTER_BASE_HREF';
class WebTemplateWarning {
WebTemplateWarning(
this.warningText,
this.lineNumber,
);
final String warningText;
final int lineNumber;
}
/// Utility class for parsing and performing operations on the contents of the /// Utility class for parsing and performing operations on the contents of the
/// index.html file. /// index.html file.
/// ///
...@@ -21,8 +31,8 @@ const String kBaseHrefPlaceholder = r'$FLUTTER_BASE_HREF'; ...@@ -21,8 +31,8 @@ const String kBaseHrefPlaceholder = r'$FLUTTER_BASE_HREF';
/// return indexHtml.getBaseHref(); /// return indexHtml.getBaseHref();
/// } /// }
/// ``` /// ```
class IndexHtml { class WebTemplate {
IndexHtml(this._content); WebTemplate(this._content);
String get content => _content; String get content => _content;
String _content; String _content;
...@@ -58,11 +68,42 @@ class IndexHtml { ...@@ -58,11 +68,42 @@ class IndexHtml {
return stripLeadingSlash(stripTrailingSlash(baseHref)); return stripLeadingSlash(stripTrailingSlash(baseHref));
} }
List<WebTemplateWarning> getWarnings() {
return <WebTemplateWarning>[
..._getWarningsForPattern(
RegExp('(const|var) serviceWorkerVersion = null'),
'Local variable for "serviceWorkerVersion" is deprecated. Use "{{flutter_service_worker_version}}" template token instead.',
),
..._getWarningsForPattern(
"navigator.serviceWorker.register('flutter_service_worker.js')",
'Manual service worker registration deprecated. Use flutter.js service worker bootstrapping instead.',
),
..._getWarningsForPattern(
'_flutter.loader.loadEntrypoint(',
'"FlutterLoader.loadEntrypoint" is deprecated. Use "FlutterLoader.load" instead.',
),
];
}
List<WebTemplateWarning> _getWarningsForPattern(Pattern pattern, String warningText) {
return <WebTemplateWarning>[
for (final Match match in pattern.allMatches(_content))
_getWarningForMatch(match, warningText)
];
}
WebTemplateWarning _getWarningForMatch(Match match, String warningText) {
final int lineCount = RegExp(r'(\r\n|\r|\n)').allMatches(_content.substring(0, match.start)).length;
return WebTemplateWarning(warningText, lineCount + 1);
}
/// Applies substitutions to the content of the index.html file. /// Applies substitutions to the content of the index.html file.
void applySubstitutions({ void applySubstitutions({
required String baseHref, required String baseHref,
required String? serviceWorkerVersion, required String? serviceWorkerVersion,
required File flutterJsFile,
String? buildConfig, String? buildConfig,
String? flutterBootstrapJs,
}) { }) {
if (_content.contains(kBaseHrefPlaceholder)) { if (_content.contains(kBaseHrefPlaceholder)) {
_content = _content.replaceAll(kBaseHrefPlaceholder, baseHref); _content = _content.replaceAll(kBaseHrefPlaceholder, baseHref);
...@@ -82,12 +123,30 @@ class IndexHtml { ...@@ -82,12 +123,30 @@ class IndexHtml {
"navigator.serviceWorker.register('flutter_service_worker.js?v=$serviceWorkerVersion')", "navigator.serviceWorker.register('flutter_service_worker.js?v=$serviceWorkerVersion')",
); );
} }
_content = _content.replaceAll(
'{{flutter_service_worker_version}}',
serviceWorkerVersion != null ? '"$serviceWorkerVersion"' : 'null',
);
if (buildConfig != null) { if (buildConfig != null) {
_content = _content.replaceFirst( _content = _content.replaceAll(
'{{flutter_build_config}}', '{{flutter_build_config}}',
buildConfig, buildConfig,
); );
} }
if (_content.contains('{{flutter_js}}')) {
_content = _content.replaceAll(
'{{flutter_js}}',
flutterJsFile.readAsStringSync(),
);
}
if (flutterBootstrapJs != null) {
_content = _content.replaceAll(
'{{flutter_bootstrap_js}}',
flutterBootstrapJs,
);
}
} }
} }
......
...@@ -31,29 +31,8 @@ ...@@ -31,29 +31,8 @@
<title>{{projectName}}</title> <title>{{projectName}}</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
const serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head> </head>
<body> <body>
<script> <script src="flutter_bootstrap.js" async></script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
onEntrypointLoaded: function(engineInitializer) {
engineInitializer.initializeEngine().then(function(appRunner) {
appRunner.runApp();
});
}
});
});
</script>
</body> </body>
</html> </html>
...@@ -12,10 +12,10 @@ import 'package:flutter_tools/src/build_system/build_system.dart'; ...@@ -12,10 +12,10 @@ import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/depfile.dart'; import 'package:flutter_tools/src/build_system/depfile.dart';
import 'package:flutter_tools/src/build_system/targets/web.dart'; import 'package:flutter_tools/src/build_system/targets/web.dart';
import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/html_utils.dart';
import 'package:flutter_tools/src/isolated/mustache_template.dart'; import 'package:flutter_tools/src/isolated/mustache_template.dart';
import 'package:flutter_tools/src/web/compile.dart'; import 'package:flutter_tools/src/web/compile.dart';
import 'package:flutter_tools/src/web/file_generators/flutter_service_worker_js.dart'; import 'package:flutter_tools/src/web/file_generators/flutter_service_worker_js.dart';
import 'package:flutter_tools/src/web_template.dart';
import '../../../src/common.dart'; import '../../../src/common.dart';
import '../../../src/fake_process_manager.dart'; import '../../../src/fake_process_manager.dart';
...@@ -144,9 +144,7 @@ void main() { ...@@ -144,9 +144,7 @@ void main() {
<!DOCTYPE html><html><base href="$kBaseHrefPlaceholder"><head></head></html> <!DOCTYPE html><html><base href="$kBaseHrefPlaceholder"><head></head></html>
'''); ''');
environment.buildDir.childFile('main.dart.js').createSync(); environment.buildDir.childFile('main.dart.js').createSync();
await WebReleaseBundle(<WebCompilerConfig>[ await WebTemplatedFiles('buildConfig').build(environment);
const JsCompilerConfig()
]).build(environment);
expect(environment.outputDir.childFile('index.html').readAsStringSync(), contains('/basehreftest/')); expect(environment.outputDir.childFile('index.html').readAsStringSync(), contains('/basehreftest/'));
})); }));
...@@ -159,9 +157,7 @@ void main() { ...@@ -159,9 +157,7 @@ void main() {
<!DOCTYPE html><html><head><base href='/basehreftest/'></head></html> <!DOCTYPE html><html><head><base href='/basehreftest/'></head></html>
'''); ''');
environment.buildDir.childFile('main.dart.js').createSync(); environment.buildDir.childFile('main.dart.js').createSync();
await WebReleaseBundle(<WebCompilerConfig>[ await WebTemplatedFiles('build config').build(environment);
const JsCompilerConfig()
]).build(environment);
expect(environment.outputDir.childFile('index.html').readAsStringSync(), contains('/basehreftest/')); expect(environment.outputDir.childFile('index.html').readAsStringSync(), contains('/basehreftest/'));
})); }));
...@@ -169,18 +165,9 @@ void main() { ...@@ -169,18 +165,9 @@ void main() {
test('WebReleaseBundle copies dart2js output and resource files to output directory', () => testbed.run(() async { test('WebReleaseBundle copies dart2js output and resource files to output directory', () => testbed.run(() async {
environment.defines[kBuildMode] = 'release'; environment.defines[kBuildMode] = 'release';
final Directory webResources = environment.projectDir.childDirectory('web'); final Directory webResources = environment.projectDir.childDirectory('web');
webResources.childFile('index.html')
..createSync(recursive: true)
..writeAsStringSync('''
<html>
<script src="main.dart.js" type="application/javascript"></script>
<script>
navigator.serviceWorker.register('flutter_service_worker.js');
</script>
</html>
''');
webResources.childFile('foo.txt') webResources.childFile('foo.txt')
.writeAsStringSync('A'); ..createSync(recursive: true)
..writeAsStringSync('A');
environment.buildDir.childFile('main.dart.js').createSync(); environment.buildDir.childFile('main.dart.js').createSync();
environment.buildDir.childFile('main.dart.js.map').createSync(); environment.buildDir.childFile('main.dart.js.map').createSync();
...@@ -206,11 +193,6 @@ void main() { ...@@ -206,11 +193,6 @@ void main() {
expect(environment.outputDir.childFile('foo.txt') expect(environment.outputDir.childFile('foo.txt')
.readAsStringSync(), 'B'); .readAsStringSync(), 'B');
// Appends number to requests for service worker only
expect(environment.outputDir.childFile('index.html').readAsStringSync(), allOf(
contains('<script src="main.dart.js" type="application/javascript">'),
contains('flutter_service_worker.js?v='),
));
})); }));
test('WebReleaseBundle copies over output files when they change', () => testbed.run(() async { test('WebReleaseBundle copies over output files when they change', () => testbed.run(() async {
......
...@@ -17,9 +17,9 @@ import 'package:flutter_tools/src/compile.dart'; ...@@ -17,9 +17,9 @@ import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/html_utils.dart';
import 'package:flutter_tools/src/isolated/devfs_web.dart'; import 'package:flutter_tools/src/isolated/devfs_web.dart';
import 'package:flutter_tools/src/web/compile.dart'; import 'package:flutter_tools/src/web/compile.dart';
import 'package:flutter_tools/src/web_template.dart';
import 'package:logging/logging.dart' as logging; import 'package:logging/logging.dart' as logging;
import 'package:package_config/package_config.dart'; import 'package:package_config/package_config.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
...@@ -344,6 +344,11 @@ void main() { ...@@ -344,6 +344,11 @@ void main() {
final Directory webDir = final Directory webDir =
globals.fs.currentDirectory.childDirectory('web')..createSync(); globals.fs.currentDirectory.childDirectory('web')..createSync();
webDir.childFile('index.html').writeAsStringSync(htmlContent); webDir.childFile('index.html').writeAsStringSync(htmlContent);
globals.fs.file(globals.fs.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
))..createSync(recursive: true)..writeAsStringSync('flutter.js content');
final Response response = await webAssetServer.handleRequest( final Response response = await webAssetServer.handleRequest(
Request('GET', Uri.parse('http://foobar/base/path/'))); Request('GET', Uri.parse('http://foobar/base/path/')));
...@@ -360,6 +365,10 @@ void main() { ...@@ -360,6 +365,10 @@ void main() {
final Directory webDir = final Directory webDir =
globals.fs.currentDirectory.childDirectory('web')..createSync(); globals.fs.currentDirectory.childDirectory('web')..createSync();
webDir.childFile('index.html').writeAsStringSync(htmlContent); webDir.childFile('index.html').writeAsStringSync(htmlContent);
globals.fs.file(globals.fs.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
))..createSync(recursive: true)..writeAsStringSync('flutter.js content');
final Response response = await webAssetServer final Response response = await webAssetServer
.handleRequest(Request('GET', Uri.parse('http://foobar/'))); .handleRequest(Request('GET', Uri.parse('http://foobar/')));
...@@ -530,6 +539,10 @@ void main() { ...@@ -530,6 +539,10 @@ void main() {
final Directory webDir = final Directory webDir =
globals.fs.currentDirectory.childDirectory('web')..createSync(); globals.fs.currentDirectory.childDirectory('web')..createSync();
webDir.childFile('index.html').writeAsStringSync(htmlContent); webDir.childFile('index.html').writeAsStringSync(htmlContent);
globals.fs.file(globals.fs.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
))..createSync(recursive: true)..writeAsStringSync('flutter.js content');
final Response response = await webAssetServer.handleRequest( final Response response = await webAssetServer.handleRequest(
Request('GET', Uri.parse('http://foobar/bar/baz'))); Request('GET', Uri.parse('http://foobar/bar/baz')));
...@@ -586,6 +599,11 @@ void main() { ...@@ -586,6 +599,11 @@ void main() {
test( test(
'serves default index.html', 'serves default index.html',
() => testbed.run(() async { () => testbed.run(() async {
globals.fs.file(globals.fs.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
))..createSync(recursive: true)..writeAsStringSync('flutter.js content');
final Response response = await webAssetServer final Response response = await webAssetServer
.handleRequest(Request('GET', Uri.parse('http://foobar/'))); .handleRequest(Request('GET', Uri.parse('http://foobar/')));
......
...@@ -17,9 +17,9 @@ import 'package:flutter_tools/src/compile.dart'; ...@@ -17,9 +17,9 @@ import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/html_utils.dart';
import 'package:flutter_tools/src/isolated/devfs_web.dart'; import 'package:flutter_tools/src/isolated/devfs_web.dart';
import 'package:flutter_tools/src/web/compile.dart'; import 'package:flutter_tools/src/web/compile.dart';
import 'package:flutter_tools/src/web_template.dart';
import 'package:logging/logging.dart' as logging; import 'package:logging/logging.dart' as logging;
import 'package:package_config/package_config.dart'; import 'package:package_config/package_config.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
...@@ -244,6 +244,10 @@ void main() { ...@@ -244,6 +244,10 @@ void main() {
.childDirectory('web') .childDirectory('web')
..createSync(); ..createSync();
webDir.childFile('index.html').writeAsStringSync(htmlContent); webDir.childFile('index.html').writeAsStringSync(htmlContent);
globals.fs.file(globals.fs.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
))..createSync(recursive: true)..writeAsStringSync('flutter.js content');
final Response response = await webAssetServer final Response response = await webAssetServer
.handleRequest(Request('GET', Uri.parse('http://foobar/base/path/'))); .handleRequest(Request('GET', Uri.parse('http://foobar/base/path/')));
...@@ -257,6 +261,11 @@ void main() { ...@@ -257,6 +261,11 @@ void main() {
final Directory webDir = globals.fs.currentDirectory.childDirectory('web') final Directory webDir = globals.fs.currentDirectory.childDirectory('web')
..createSync(); ..createSync();
webDir.childFile('index.html').writeAsStringSync(htmlContent); webDir.childFile('index.html').writeAsStringSync(htmlContent);
globals.fs.file(globals.fs.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
))..createSync(recursive: true)..writeAsStringSync('flutter.js content');
final Response response = await webAssetServer final Response response = await webAssetServer
.handleRequest(Request('GET', Uri.parse('http://foobar/'))); .handleRequest(Request('GET', Uri.parse('http://foobar/')));
...@@ -404,6 +413,10 @@ void main() { ...@@ -404,6 +413,10 @@ void main() {
.childDirectory('web') .childDirectory('web')
..createSync(); ..createSync();
webDir.childFile('index.html').writeAsStringSync(htmlContent); webDir.childFile('index.html').writeAsStringSync(htmlContent);
globals.fs.file(globals.fs.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
))..createSync(recursive: true)..writeAsStringSync('flutter.js content');
final Response response = await webAssetServer final Response response = await webAssetServer
.handleRequest(Request('GET', Uri.parse('http://foobar/bar/baz'))); .handleRequest(Request('GET', Uri.parse('http://foobar/bar/baz')));
...@@ -454,6 +467,11 @@ void main() { ...@@ -454,6 +467,11 @@ void main() {
})); }));
test('serves default index.html', () => testbed.run(() async { test('serves default index.html', () => testbed.run(() async {
globals.fs.file(globals.fs.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
))..createSync(recursive: true)..writeAsStringSync('flutter.js content');
final Response response = await webAssetServer final Response response = await webAssetServer
.handleRequest(Request('GET', Uri.parse('http://foobar/'))); .handleRequest(Request('GET', Uri.parse('http://foobar/')));
...@@ -830,6 +848,10 @@ void main() { ...@@ -830,6 +848,10 @@ void main() {
.getHostArtifact(HostArtifact.webPrecompiledAmdCanvaskitSoundSdk).path; .getHostArtifact(HostArtifact.webPrecompiledAmdCanvaskitSoundSdk).path;
final String webPrecompiledCanvaskitSdkSourcemaps = globals.artifacts! final String webPrecompiledCanvaskitSdkSourcemaps = globals.artifacts!
.getHostArtifact(HostArtifact.webPrecompiledAmdCanvaskitSoundSdkSourcemaps).path; .getHostArtifact(HostArtifact.webPrecompiledAmdCanvaskitSoundSdkSourcemaps).path;
final String flutterJs = globals.fs.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
);
globals.fs.file(webPrecompiledSdk) globals.fs.file(webPrecompiledSdk)
..createSync(recursive: true) ..createSync(recursive: true)
..writeAsStringSync('HELLO'); ..writeAsStringSync('HELLO');
...@@ -842,6 +864,9 @@ void main() { ...@@ -842,6 +864,9 @@ void main() {
globals.fs.file(webPrecompiledCanvaskitSdkSourcemaps) globals.fs.file(webPrecompiledCanvaskitSdkSourcemaps)
..createSync(recursive: true) ..createSync(recursive: true)
..writeAsStringSync('CHUM'); ..writeAsStringSync('CHUM');
globals.fs.file(flutterJs)
..createSync(recursive: true)
..writeAsStringSync('(flutter.js content)');
await webDevFS.update( await webDevFS.update(
mainUri: globals.fs.file(globals.fs.path.join('lib', 'main.dart')).uri, mainUri: globals.fs.file(globals.fs.path.join('lib', 'main.dart')).uri,
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_tools/src/html_utils.dart'; import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/web_template.dart';
import '../src/common.dart'; import '../src/common.dart';
...@@ -44,6 +46,90 @@ const String htmlSample2 = ''' ...@@ -44,6 +46,90 @@ const String htmlSample2 = '''
</html> </html>
'''; ''';
const String htmlSampleInlineFlutterJsBootstrap = '''
<!DOCTYPE html>
<html>
<head>
<title></title>
<base href="/foo/222/">
<meta charset="utf-8">
<link rel="icon" type="image/png" href="favicon.png"/>
</head>
<body>
<div></div>
<script>
{{flutter_js}}
{{flutter_build_config}}
_flutter.loader.load({
serviceWorker: {
serviceWorkerVersion: {{flutter_service_worker_version}},
},
});
</script>
</body>
</html>
''';
const String htmlSampleInlineFlutterJsBootstrapOutput = '''
<!DOCTYPE html>
<html>
<head>
<title></title>
<base href="/foo/222/">
<meta charset="utf-8">
<link rel="icon" type="image/png" href="favicon.png"/>
</head>
<body>
<div></div>
<script>
(flutter.js content)
(build config)
_flutter.loader.load({
serviceWorker: {
serviceWorkerVersion: "(service worker version)",
},
});
</script>
</body>
</html>
''';
const String htmlSampleFullFlutterBootstrapReplacement = '''
<!DOCTYPE html>
<html>
<head>
<title></title>
<base href="/foo/222/">
<meta charset="utf-8">
<link rel="icon" type="image/png" href="favicon.png"/>
</head>
<body>
<div></div>
<script>
{{flutter_bootstrap_js}}
</script>
</body>
</html>
''';
const String htmlSampleFullFlutterBootstrapReplacementOutput = '''
<!DOCTYPE html>
<html>
<head>
<title></title>
<base href="/foo/222/">
<meta charset="utf-8">
<link rel="icon" type="image/png" href="favicon.png"/>
</head>
<body>
<div></div>
<script>
(flutter bootstrap script)
</script>
</body>
</html>
''';
const String htmlSampleLegacyVar = ''' const String htmlSampleLegacyVar = '''
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
...@@ -66,6 +152,32 @@ const String htmlSampleLegacyVar = ''' ...@@ -66,6 +152,32 @@ const String htmlSampleLegacyVar = '''
</html> </html>
'''; ''';
const String htmlSampleLegacyLoadEntrypoint = '''
<!DOCTYPE html>
<html>
<head>
<title></title>
<base href="$kBaseHrefPlaceholder">
<meta charset="utf-8">
<link rel="icon" type="image/png" href="favicon.png"/>
<script src="flutter.js" defer></script>
</head>
<body>
<div></div>
<script>
window.addEventListener('load', function(ev) {
_flutter.loader.loadEntrypoint({
onEntrypointLoaded: function(engineInitializer) {
engineInitializer.initializeEngine().then(function(appRunner) {
appRunner.runApp();
});
});
});
</script>
</body>
</html>
''';
String htmlSample2Replaced({ String htmlSample2Replaced({
required String baseHref, required String baseHref,
required String serviceWorkerVersion, required String serviceWorkerVersion,
...@@ -108,37 +220,42 @@ const String htmlSample3 = ''' ...@@ -108,37 +220,42 @@ const String htmlSample3 = '''
'''; ''';
void main() { void main() {
final MemoryFileSystem fs = MemoryFileSystem();
final File flutterJs = fs.file('flutter.js');
flutterJs.writeAsStringSync('(flutter.js content)');
test('can parse baseHref', () { test('can parse baseHref', () {
expect(IndexHtml('<base href="/foo/111/">').getBaseHref(), 'foo/111'); expect(WebTemplate('<base href="/foo/111/">').getBaseHref(), 'foo/111');
expect(IndexHtml(htmlSample1).getBaseHref(), 'foo/222'); expect(WebTemplate(htmlSample1).getBaseHref(), 'foo/222');
expect(IndexHtml(htmlSample2).getBaseHref(), ''); // Placeholder base href. expect(WebTemplate(htmlSample2).getBaseHref(), ''); // Placeholder base href.
}); });
test('handles missing baseHref', () { test('handles missing baseHref', () {
expect(IndexHtml('').getBaseHref(), ''); expect(WebTemplate('').getBaseHref(), '');
expect(IndexHtml('<base>').getBaseHref(), ''); expect(WebTemplate('<base>').getBaseHref(), '');
expect(IndexHtml(htmlSample3).getBaseHref(), ''); expect(WebTemplate(htmlSample3).getBaseHref(), '');
}); });
test('throws on invalid baseHref', () { test('throws on invalid baseHref', () {
expect(() => IndexHtml('<base href>').getBaseHref(), throwsToolExit()); expect(() => WebTemplate('<base href>').getBaseHref(), throwsToolExit());
expect(() => IndexHtml('<base href="">').getBaseHref(), throwsToolExit()); expect(() => WebTemplate('<base href="">').getBaseHref(), throwsToolExit());
expect(() => IndexHtml('<base href="foo/111">').getBaseHref(), throwsToolExit()); expect(() => WebTemplate('<base href="foo/111">').getBaseHref(), throwsToolExit());
expect( expect(
() => IndexHtml('<base href="foo/111/">').getBaseHref(), () => WebTemplate('<base href="foo/111/">').getBaseHref(),
throwsToolExit(), throwsToolExit(),
); );
expect( expect(
() => IndexHtml('<base href="/foo/111">').getBaseHref(), () => WebTemplate('<base href="/foo/111">').getBaseHref(),
throwsToolExit(), throwsToolExit(),
); );
}); });
test('applies substitutions', () { test('applies substitutions', () {
final IndexHtml indexHtml = IndexHtml(htmlSample2); final WebTemplate indexHtml = WebTemplate(htmlSample2);
indexHtml.applySubstitutions( indexHtml.applySubstitutions(
baseHref: '/foo/333/', baseHref: '/foo/333/',
serviceWorkerVersion: 'v123xyz', serviceWorkerVersion: 'v123xyz',
flutterJsFile: flutterJs,
); );
expect( expect(
indexHtml.content, indexHtml.content,
...@@ -150,10 +267,11 @@ void main() { ...@@ -150,10 +267,11 @@ void main() {
}); });
test('applies substitutions with legacy var version syntax', () { test('applies substitutions with legacy var version syntax', () {
final IndexHtml indexHtml = IndexHtml(htmlSampleLegacyVar); final WebTemplate indexHtml = WebTemplate(htmlSampleLegacyVar);
indexHtml.applySubstitutions( indexHtml.applySubstitutions(
baseHref: '/foo/333/', baseHref: '/foo/333/',
serviceWorkerVersion: 'v123xyz', serviceWorkerVersion: 'v123xyz',
flutterJsFile: flutterJs,
); );
expect( expect(
indexHtml.content, indexHtml.content,
...@@ -164,15 +282,60 @@ void main() { ...@@ -164,15 +282,60 @@ void main() {
); );
}); });
test('applies substitutions to inline flutter.js bootstrap script', () {
final WebTemplate indexHtml = WebTemplate(htmlSampleInlineFlutterJsBootstrap);
expect(indexHtml.getWarnings(), isEmpty);
indexHtml.applySubstitutions(
baseHref: '/',
serviceWorkerVersion: '(service worker version)',
flutterJsFile: flutterJs,
buildConfig: '(build config)',
);
expect(indexHtml.content, htmlSampleInlineFlutterJsBootstrapOutput);
});
test('applies substitutions to full flutter_bootstrap.js replacement', () {
final WebTemplate indexHtml = WebTemplate(htmlSampleFullFlutterBootstrapReplacement);
expect(indexHtml.getWarnings(), isEmpty);
indexHtml.applySubstitutions(
baseHref: '/',
serviceWorkerVersion: '(service worker version)',
flutterJsFile: flutterJs,
buildConfig: '(build config)',
flutterBootstrapJs: '(flutter bootstrap script)',
);
expect(indexHtml.content, htmlSampleFullFlutterBootstrapReplacementOutput);
});
test('re-parses after substitutions', () { test('re-parses after substitutions', () {
final IndexHtml indexHtml = IndexHtml(htmlSample2); final WebTemplate indexHtml = WebTemplate(htmlSample2);
expect(indexHtml.getBaseHref(), ''); // Placeholder base href. expect(indexHtml.getBaseHref(), ''); // Placeholder base href.
indexHtml.applySubstitutions( indexHtml.applySubstitutions(
baseHref: '/foo/333/', baseHref: '/foo/333/',
serviceWorkerVersion: 'v123xyz', serviceWorkerVersion: 'v123xyz',
flutterJsFile: flutterJs,
); );
// The parsed base href should be updated after substitutions. // The parsed base href should be updated after substitutions.
expect(indexHtml.getBaseHref(), 'foo/333'); expect(indexHtml.getBaseHref(), 'foo/333');
}); });
test('warns on legacy service worker patterns', () {
final WebTemplate indexHtml = WebTemplate(htmlSampleLegacyVar);
final List<WebTemplateWarning> warnings = indexHtml.getWarnings();
expect(warnings.length, 2);
expect(warnings.where((WebTemplateWarning warning) => warning.lineNumber == 13), isNotEmpty);
expect(warnings.where((WebTemplateWarning warning) => warning.lineNumber == 16), isNotEmpty);
});
test('warns on legacy FlutterLoader.loadEntrypoint', () {
final WebTemplate indexHtml = WebTemplate(htmlSampleLegacyLoadEntrypoint);
final List<WebTemplateWarning> warnings = indexHtml.getWarnings();
expect(warnings.length, 1);
expect(warnings.single.lineNumber, 14);
});
} }
...@@ -21,6 +21,8 @@ void main() async { ...@@ -21,6 +21,8 @@ void main() async {
await _testProject(HotReloadProject(indexHtml: indexHtmlFlutterJsPromisesShort), name: 'flutter.js (promises, short)'); await _testProject(HotReloadProject(indexHtml: indexHtmlFlutterJsPromisesShort), name: 'flutter.js (promises, short)');
await _testProject(HotReloadProject(indexHtml: indexHtmlFlutterJsLoad), name: 'flutter.js (load)'); await _testProject(HotReloadProject(indexHtml: indexHtmlFlutterJsLoad), name: 'flutter.js (load)');
await _testProject(HotReloadProject(indexHtml: indexHtmlNoFlutterJs), name: 'No flutter.js'); await _testProject(HotReloadProject(indexHtml: indexHtmlNoFlutterJs), name: 'No flutter.js');
await _testProject(HotReloadProject(indexHtml: indexHtmlWithFlutterBootstrapScriptTag), name: 'Using flutter_bootstrap.js script tag');
await _testProject(HotReloadProject(indexHtml: indexHtmlWithInlinedFlutterBootstrapScript), name: 'Using inlined flutter_bootstrap.js');
} }
Future<void> _testProject(HotReloadProject project, {String name = 'Default'}) async { Future<void> _testProject(HotReloadProject project, {String name = 'Default'}) async {
...@@ -73,8 +75,7 @@ Future<void> _testProject(HotReloadProject project, {String name = 'Default'}) a ...@@ -73,8 +75,7 @@ Future<void> _testProject(HotReloadProject project, {String name = 'Default'}) a
completer.complete(); completer.complete();
} }
}); });
await flutter.run(chrome: true, await flutter.run(chrome: true, additionalCommandArgs: <String>['--verbose', '--web-renderer=canvaskit']);
additionalCommandArgs: <String>['--dart-define=FLUTTER_WEB_USE_SKIA=true', '--verbose']);
project.uncommentHotReloadPrint(); project.uncommentHotReloadPrint();
try { try {
await flutter.hotRestart(); await flutter.hotRestart();
......
...@@ -198,3 +198,53 @@ $initScript ...@@ -198,3 +198,53 @@ $initScript
</body> </body>
</html> </html>
'''; ''';
/// index.html using flutter bootstrap script
const String indexHtmlWithFlutterBootstrapScriptTag = '''
<!DOCTYPE HTML>
<!-- Copyright 2014 The Flutter Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<html>
<head>
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<title>Web Test</title>
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test">
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
''';
/// index.html using flutter bootstrap script
const String indexHtmlWithInlinedFlutterBootstrapScript = '''
<!DOCTYPE HTML>
<!-- Copyright 2014 The Flutter Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<html>
<head>
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<title>Web Test</title>
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test">
<link rel="manifest" href="manifest.json">
</head>
<body>
<script>
{{flutter_bootstrap_js}}
</script>
</body>
</html>
''';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment