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. -->
const serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<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>
<script src="flutter_bootstrap.js" async></script>
</body>
</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. -->
<head>
<meta charset="UTF-8">
<title>Web Benchmarks</title>
<script src="flutter.js"></script>
</head>
<body>
<script>
{{flutter_build_config}}
_flutter.loader.load({
config: {
canvasKitBaseUrl: '/canvaskit/',
}
});
</script>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
......@@ -163,7 +163,11 @@ Future<void> _waitForAppToLoad(
print('Waiting for app to load $waitForCounts');
await Future.any(<Future<Object?>>[
() async {
int tries = 1;
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));
}
}(),
......@@ -304,15 +308,16 @@ Future<void> runWebServiceWorkerTest({
'flutter.js': 1,
'main.dart.js': 1,
'flutter_service_worker.js': 1,
'flutter_bootstrap.js': 1,
'assets/FontManifest.json': 1,
'assets/AssetManifest.bin.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 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)
...<String, int>{
'manifest.json': 1,
'favicon.ico': 1,
'favicon.png': 1,
},
});
expect(reportedVersion, '1');
......@@ -346,12 +351,13 @@ Future<void> runWebServiceWorkerTest({
if (shouldExpectFlutterJs)
'flutter.js': 1,
'flutter_service_worker.js': 2,
'flutter_bootstrap.js': 1,
'main.dart.js': 1,
'assets/AssetManifest.bin.json': 1,
'assets/FontManifest.json': 1,
'CLOSE': 1,
if (!headless)
'favicon.ico': 1,
'favicon.png': 1,
});
expect(reportedVersion, '2');
......@@ -377,14 +383,15 @@ Future<void> runWebServiceWorkerTest({
'main.dart.js': 1,
'assets/FontManifest.json': 1,
'flutter_service_worker.js': 1,
'flutter_bootstrap.js': 1,
'assets/AssetManifest.bin.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 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)
...<String, int>{
'manifest.json': 1,
'favicon.ico': 1,
'favicon.png': 1,
},
});
......@@ -429,6 +436,7 @@ Future<void> runWebServiceWorkerTest({
if (shouldExpectFlutterJs)
'flutter.js': 1,
'flutter_service_worker.js': 2,
'flutter_bootstrap.js': 1,
'main.dart.js': 1,
'assets/AssetManifest.bin.json': 1,
'assets/FontManifest.json': 1,
......@@ -436,7 +444,7 @@ Future<void> runWebServiceWorkerTest({
if (!headless)
...<String, int>{
'manifest.json': 1,
'favicon.ico': 1,
'favicon.png': 1,
},
});
......@@ -508,8 +516,8 @@ Future<void> runWebServiceWorkerTestWithCachingResources({
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)');
try {
......@@ -534,14 +542,15 @@ Future<void> runWebServiceWorkerTestWithCachingResources({
'flutter.js': 1,
'main.dart.js': 1,
'flutter_service_worker.js': 1,
'flutter_bootstrap.js': usesFlutterBootstrapJs ? 2 : 1,
'assets/FontManifest.json': 1,
'assets/AssetManifest.bin.json': 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)
...<String, int>{
'manifest.json': 1,
'favicon.ico': 1,
'favicon.png': 1,
},
});
......@@ -593,13 +602,14 @@ Future<void> runWebServiceWorkerTestWithCachingResources({
'flutter.js': 1,
'main.dart.js': 1,
'flutter_service_worker.js': 2,
'flutter_bootstrap.js': usesFlutterBootstrapJs ? 2 : 1,
'assets/FontManifest.json': 1,
'assets/AssetManifest.bin.json': 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)
...<String, int>{
'favicon.ico': 1,
'favicon.png': 1,
},
});
} finally {
......@@ -682,11 +692,11 @@ Future<void> runWebServiceWorkerTestWithBlockedServiceWorkers({
'assets/FontManifest.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 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)
...<String, int>{
'manifest.json': 1,
'favicon.ico': 1,
'favicon.png': 1,
},
});
} finally {
......@@ -770,14 +780,15 @@ Future<void> runWebServiceWorkerTestWithCustomServiceWorkerVersion({
'main.dart.js': 1,
'CLOSE': 1,
'flutter_service_worker.js': 1,
'flutter_bootstrap.js': 1,
'assets/FontManifest.json': 1,
'assets/AssetManifest.bin.json': 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)
...<String, int>{
'manifest.json': 1,
'favicon.ico': 1,
'favicon.png': 1,
},
});
......@@ -794,11 +805,11 @@ Future<void> runWebServiceWorkerTestWithCustomServiceWorkerVersion({
'assets/FontManifest.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 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)
...<String, int>{
'manifest.json': 1,
'favicon.ico': 1,
'favicon.png': 1,
},
});
......@@ -816,11 +827,11 @@ Future<void> runWebServiceWorkerTestWithCustomServiceWorkerVersion({
'assets/FontManifest.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 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)
...<String, int>{
'manifest.json': 1,
'favicon.ico': 1,
'favicon.png': 1,
},
});
} finally {
......
......@@ -5,7 +5,9 @@
import 'package:flutter/material.dart';
Future<void> main() async {
runApp(const Scaffold(
runApp(const Directionality(
textDirection: TextDirection.ltr,
child: Scaffold(
body: Center(
child: Column(
children: <Widget>[
......@@ -14,5 +16,5 @@ Future<void> main() async {
],
),
),
));
)));
}
......@@ -12,6 +12,9 @@ found in the LICENSE file. -->
<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">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json">
</head>
<body>
......
......@@ -12,6 +12,9 @@ found in the LICENSE file. -->
<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">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json">
<script>
......
......@@ -12,6 +12,9 @@ found in the LICENSE file. -->
<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">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
......
......@@ -13,6 +13,9 @@ found in the LICENSE file. -->
<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">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
......
......@@ -16,6 +16,9 @@ found in the LICENSE file. -->
<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">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json">
<script nonce="SOME_NONCE">
// The value below is injected by flutter build, do not touch.
......
......@@ -16,6 +16,9 @@ found in the LICENSE file. -->
<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">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
......
......@@ -12,6 +12,9 @@ found in the LICENSE file. -->
<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">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
......
......@@ -12,6 +12,9 @@ found in the LICENSE file. -->
<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">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
......
......@@ -12,6 +12,9 @@ found in the LICENSE file. -->
<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">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="manifest" href="manifest.json">
</head>
<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. -->
<html>
<head>
<title>Web Integration Tests</title>
<script src="flutter.js"></script>
</head>
<body>
<script>
{{flutter_build_config}}
_flutter.loader.load({
config: {
// Use the local CanvasKit bundle instead of the CDN to reduce test flakiness.
canvasKitBaseUrl: "/canvaskit/",
},
});
</script>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
......@@ -17,11 +17,12 @@ import '../../dart/language_version.dart';
import '../../dart/package_map.dart';
import '../../flutter_plugins.dart';
import '../../globals.dart' as globals;
import '../../html_utils.dart';
import '../../project.dart';
import '../../web/bootstrap.dart';
import '../../web/compile.dart';
import '../../web/file_generators/flutter_service_worker_js.dart';
import '../../web/file_generators/main_dart.dart' as main_dart;
import '../../web_template.dart';
import '../build_system.dart';
import '../depfile.dart';
import '../exceptions.dart';
......@@ -328,18 +329,37 @@ class Dart2WasmTarget extends Dart2WebTarget {
/// Unpacks the dart2js or dart2wasm compilation and resources to a given
/// output directory.
class WebReleaseBundle extends Target {
WebReleaseBundle(List<WebCompilerConfig> configs) : this._withTargets(
configs.map((WebCompilerConfig config) =>
WebReleaseBundle(List<WebCompilerConfig> configs) : this._(
compileTargets: configs.map((WebCompilerConfig config) =>
switch (config) {
WasmCompilerConfig() => Dart2WasmTarget(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 WebTemplatedFiles templatedFilesTarget;
List<String> get buildFiles => compileTargets.fold(
const Iterable<String>.empty(),
......@@ -350,7 +370,10 @@ class WebReleaseBundle extends Target {
String get name => 'web_release_bundle';
@override
List<Target> get dependencies => compileTargets;
List<Target> get dependencies => <Target>[
...compileTargets,
templatedFilesTarget,
];
@override
List<Source> get inputs => <Source>[
......@@ -371,11 +394,12 @@ class WebReleaseBundle extends Target {
@override
Future<void> build(Environment environment) async {
final FileSystem fileSystem = environment.fileSystem;
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)) {
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 {
// Copy other resource files out of web/ directory.
final List<File> outputResourcesFiles = <File>[];
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.fileSystem.path.relative(inputFile.path, from: webResources.path)));
relativePath));
if (!outputFile.parent.existsSync()) {
outputFile.parent.createSync(recursive: true);
}
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);
}
final Depfile resourceFile = Depfile(inputResourceFiles, outputResourcesFiles);
......@@ -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
/// CanvasKit.
///
......@@ -596,6 +708,7 @@ class WebServiceWorker extends Target {
'main.dart.mjs',
],
'index.html',
'flutter_bootstrap.js',
if (urlToHash.containsKey('assets/AssetManifest.bin.json'))
'assets/AssetManifest.bin.json',
if (urlToHash.containsKey('assets/FontManifest.json'))
......
......@@ -8,13 +8,13 @@ import '../base/utils.dart';
import '../build_info.dart';
import '../features.dart';
import '../globals.dart' as globals;
import '../html_utils.dart';
import '../project.dart';
import '../runner/flutter_command.dart'
show DevelopmentArtifact, FlutterCommandResult, FlutterOptions;
import '../web/compile.dart';
import '../web/file_generators/flutter_service_worker_js.dart';
import '../web/web_constants.dart';
import '../web_template.dart';
import 'build.dart';
class BuildWebCommand extends BuildSubCommand {
......
......@@ -34,13 +34,13 @@ import '../dart/package_map.dart';
import '../devfs.dart';
import '../device.dart';
import '../globals.dart' as globals;
import '../html_utils.dart';
import '../project.dart';
import '../vmservice.dart';
import '../web/bootstrap.dart';
import '../web/chrome.dart';
import '../web/compile.dart';
import '../web/memory_fs.dart';
import '../web_template.dart';
typedef DwdsLauncher = Future<Dwds> Function({
required AssetReader assetReader,
......@@ -120,7 +120,7 @@ class WebAssetServer implements AssetReader {
this._nullSafetyMode,
this._ddcModuleSystem, {
required this.webRenderer,
}) : basePath = _getIndexHtml().getBaseHref();
}) : basePath = _getWebTemplate('index.html', _kDefaultIndex).getBaseHref();
// Fallback to "application/octet-stream" on null which
// makes no claims as to the structure of the data.
......@@ -386,7 +386,11 @@ class WebAssetServer implements AssetReader {
// If the response is `/`, then we are requesting the index file.
if (requestPath == '/' || requestPath.isEmpty) {
return _serveIndex();
return _serveIndexHtml();
}
if (requestPath == 'flutter_bootstrap.js') {
return _serveFlutterBootstrapJs();
}
final Map<String, String> headers = <String, String>{};
......@@ -478,7 +482,7 @@ class WebAssetServer implements AssetReader {
requestPath.startsWith('canvaskit/')) {
return shelf.Response.notFound('');
}
return _serveIndex();
return _serveIndexHtml();
}
// For real files, use a serialized file stat plus path as a revision.
......@@ -524,8 +528,7 @@ class WebAssetServer implements AssetReader {
/// Determines what rendering backed to use.
final WebRendererMode webRenderer;
shelf.Response _serveIndex() {
final IndexHtml indexHtml = _getIndexHtml();
String get _buildConfigString {
final Map<String, dynamic> buildConfig = <String, dynamic>{
'engineRevision': globals.flutterVersion.engineRevision,
'builds': <dynamic>[
......@@ -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(
// Currently, we don't support --base-href for the "run" command.
baseHref: '/',
serviceWorkerVersion: null,
buildConfig: buildConfigString,
buildConfig: _buildConfigString,
flutterJsFile: _flutterJsFile,
flutterBootstrapJs: _flutterBootstrapJsContent,
);
final Map<String, String> headers = <String, String>{
return shelf.Response.ok(indexHtml.content, headers: <String, String>{
HttpHeaders.contentTypeHeader: 'text/html',
};
return shelf.Response.ok(indexHtml.content, headers: headers);
});
}
// Attempt to resolve `path` to a dart file.
......@@ -860,6 +896,21 @@ class WebDevFS implements DevFS {
@override
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
Future<UpdateFSReport> update({
required Uri mainUri,
......@@ -950,6 +1001,8 @@ class WebDevFS implements DevFS {
);
}
}
await _validateTemplateFile('index.html');
await _validateTemplateFile('flutter_bootstrap.js');
final DateTime candidateCompileTime = DateTime.now();
if (fullRestart) {
generator.reset();
......@@ -1173,10 +1226,10 @@ String? _stripBasePath(String path, String basePath) {
return stripLeadingSlash(path);
}
IndexHtml _getIndexHtml() {
final File indexHtml =
globals.fs.currentDirectory.childDirectory('web').childFile('index.html');
WebTemplate _getWebTemplate(String filename, String fallbackContent) {
final File template =
globals.fs.currentDirectory.childDirectory('web').childFile(filename);
final String htmlContent =
indexHtml.existsSync() ? indexHtml.readAsStringSync() : _kDefaultIndex;
return IndexHtml(htmlContent);
template.existsSync() ? template.readAsStringSync() : fallbackContent;
return WebTemplate(htmlContent);
}
......@@ -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';
import 'package:html/parser.dart';
import 'base/common.dart';
import 'base/file_system.dart';
/// Placeholder for 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
/// index.html file.
///
......@@ -21,8 +31,8 @@ const String kBaseHrefPlaceholder = r'$FLUTTER_BASE_HREF';
/// return indexHtml.getBaseHref();
/// }
/// ```
class IndexHtml {
IndexHtml(this._content);
class WebTemplate {
WebTemplate(this._content);
String get content => _content;
String _content;
......@@ -58,11 +68,42 @@ class IndexHtml {
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.
void applySubstitutions({
required String baseHref,
required String? serviceWorkerVersion,
required File flutterJsFile,
String? buildConfig,
String? flutterBootstrapJs,
}) {
if (_content.contains(kBaseHrefPlaceholder)) {
_content = _content.replaceAll(kBaseHrefPlaceholder, baseHref);
......@@ -82,12 +123,30 @@ class IndexHtml {
"navigator.serviceWorker.register('flutter_service_worker.js?v=$serviceWorkerVersion')",
);
}
_content = _content.replaceAll(
'{{flutter_service_worker_version}}',
serviceWorkerVersion != null ? '"$serviceWorkerVersion"' : 'null',
);
if (buildConfig != null) {
_content = _content.replaceFirst(
_content = _content.replaceAll(
'{{flutter_build_config}}',
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 @@
<title>{{projectName}}</title>
<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>
<body>
<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>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
......@@ -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/targets/web.dart';
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/web/compile.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/fake_process_manager.dart';
......@@ -144,9 +144,7 @@ void main() {
<!DOCTYPE html><html><base href="$kBaseHrefPlaceholder"><head></head></html>
''');
environment.buildDir.childFile('main.dart.js').createSync();
await WebReleaseBundle(<WebCompilerConfig>[
const JsCompilerConfig()
]).build(environment);
await WebTemplatedFiles('buildConfig').build(environment);
expect(environment.outputDir.childFile('index.html').readAsStringSync(), contains('/basehreftest/'));
}));
......@@ -159,9 +157,7 @@ void main() {
<!DOCTYPE html><html><head><base href='/basehreftest/'></head></html>
''');
environment.buildDir.childFile('main.dart.js').createSync();
await WebReleaseBundle(<WebCompilerConfig>[
const JsCompilerConfig()
]).build(environment);
await WebTemplatedFiles('build config').build(environment);
expect(environment.outputDir.childFile('index.html').readAsStringSync(), contains('/basehreftest/'));
}));
......@@ -169,18 +165,9 @@ void main() {
test('WebReleaseBundle copies dart2js output and resource files to output directory', () => testbed.run(() async {
environment.defines[kBuildMode] = 'release';
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')
.writeAsStringSync('A');
..createSync(recursive: true)
..writeAsStringSync('A');
environment.buildDir.childFile('main.dart.js').createSync();
environment.buildDir.childFile('main.dart.js.map').createSync();
......@@ -206,11 +193,6 @@ void main() {
expect(environment.outputDir.childFile('foo.txt')
.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 {
......
......@@ -17,9 +17,9 @@ import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/devfs.dart';
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/web/compile.dart';
import 'package:flutter_tools/src/web_template.dart';
import 'package:logging/logging.dart' as logging;
import 'package:package_config/package_config.dart';
import 'package:shelf/shelf.dart';
......@@ -344,6 +344,11 @@ void main() {
final Directory webDir =
globals.fs.currentDirectory.childDirectory('web')..createSync();
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(
Request('GET', Uri.parse('http://foobar/base/path/')));
......@@ -360,6 +365,10 @@ void main() {
final Directory webDir =
globals.fs.currentDirectory.childDirectory('web')..createSync();
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(Request('GET', Uri.parse('http://foobar/')));
......@@ -530,6 +539,10 @@ void main() {
final Directory webDir =
globals.fs.currentDirectory.childDirectory('web')..createSync();
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(
Request('GET', Uri.parse('http://foobar/bar/baz')));
......@@ -586,6 +599,11 @@ void main() {
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
.handleRequest(Request('GET', Uri.parse('http://foobar/')));
......
......@@ -17,9 +17,9 @@ import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/devfs.dart';
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/web/compile.dart';
import 'package:flutter_tools/src/web_template.dart';
import 'package:logging/logging.dart' as logging;
import 'package:package_config/package_config.dart';
import 'package:shelf/shelf.dart';
......@@ -244,6 +244,10 @@ void main() {
.childDirectory('web')
..createSync();
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(Request('GET', Uri.parse('http://foobar/base/path/')));
......@@ -257,6 +261,11 @@ void main() {
final Directory webDir = globals.fs.currentDirectory.childDirectory('web')
..createSync();
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(Request('GET', Uri.parse('http://foobar/')));
......@@ -404,6 +413,10 @@ void main() {
.childDirectory('web')
..createSync();
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(Request('GET', Uri.parse('http://foobar/bar/baz')));
......@@ -454,6 +467,11 @@ void main() {
}));
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
.handleRequest(Request('GET', Uri.parse('http://foobar/')));
......@@ -830,6 +848,10 @@ void main() {
.getHostArtifact(HostArtifact.webPrecompiledAmdCanvaskitSoundSdk).path;
final String webPrecompiledCanvaskitSdkSourcemaps = globals.artifacts!
.getHostArtifact(HostArtifact.webPrecompiledAmdCanvaskitSoundSdkSourcemaps).path;
final String flutterJs = globals.fs.path.join(
globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
);
globals.fs.file(webPrecompiledSdk)
..createSync(recursive: true)
..writeAsStringSync('HELLO');
......@@ -842,6 +864,9 @@ void main() {
globals.fs.file(webPrecompiledCanvaskitSdkSourcemaps)
..createSync(recursive: true)
..writeAsStringSync('CHUM');
globals.fs.file(flutterJs)
..createSync(recursive: true)
..writeAsStringSync('(flutter.js content)');
await webDevFS.update(
mainUri: globals.fs.file(globals.fs.path.join('lib', 'main.dart')).uri,
......
......@@ -2,7 +2,9 @@
// 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/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';
......@@ -44,6 +46,90 @@ const String htmlSample2 = '''
</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 = '''
<!DOCTYPE html>
<html>
......@@ -66,6 +152,32 @@ const String htmlSampleLegacyVar = '''
</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({
required String baseHref,
required String serviceWorkerVersion,
......@@ -108,37 +220,42 @@ const String htmlSample3 = '''
''';
void main() {
final MemoryFileSystem fs = MemoryFileSystem();
final File flutterJs = fs.file('flutter.js');
flutterJs.writeAsStringSync('(flutter.js content)');
test('can parse baseHref', () {
expect(IndexHtml('<base href="/foo/111/">').getBaseHref(), 'foo/111');
expect(IndexHtml(htmlSample1).getBaseHref(), 'foo/222');
expect(IndexHtml(htmlSample2).getBaseHref(), ''); // Placeholder base href.
expect(WebTemplate('<base href="/foo/111/">').getBaseHref(), 'foo/111');
expect(WebTemplate(htmlSample1).getBaseHref(), 'foo/222');
expect(WebTemplate(htmlSample2).getBaseHref(), ''); // Placeholder base href.
});
test('handles missing baseHref', () {
expect(IndexHtml('').getBaseHref(), '');
expect(IndexHtml('<base>').getBaseHref(), '');
expect(IndexHtml(htmlSample3).getBaseHref(), '');
expect(WebTemplate('').getBaseHref(), '');
expect(WebTemplate('<base>').getBaseHref(), '');
expect(WebTemplate(htmlSample3).getBaseHref(), '');
});
test('throws on invalid baseHref', () {
expect(() => IndexHtml('<base href>').getBaseHref(), throwsToolExit());
expect(() => IndexHtml('<base href="">').getBaseHref(), throwsToolExit());
expect(() => IndexHtml('<base href="foo/111">').getBaseHref(), throwsToolExit());
expect(() => WebTemplate('<base href>').getBaseHref(), throwsToolExit());
expect(() => WebTemplate('<base href="">').getBaseHref(), throwsToolExit());
expect(() => WebTemplate('<base href="foo/111">').getBaseHref(), throwsToolExit());
expect(
() => IndexHtml('<base href="foo/111/">').getBaseHref(),
() => WebTemplate('<base href="foo/111/">').getBaseHref(),
throwsToolExit(),
);
expect(
() => IndexHtml('<base href="/foo/111">').getBaseHref(),
() => WebTemplate('<base href="/foo/111">').getBaseHref(),
throwsToolExit(),
);
});
test('applies substitutions', () {
final IndexHtml indexHtml = IndexHtml(htmlSample2);
final WebTemplate indexHtml = WebTemplate(htmlSample2);
indexHtml.applySubstitutions(
baseHref: '/foo/333/',
serviceWorkerVersion: 'v123xyz',
flutterJsFile: flutterJs,
);
expect(
indexHtml.content,
......@@ -150,10 +267,11 @@ void main() {
});
test('applies substitutions with legacy var version syntax', () {
final IndexHtml indexHtml = IndexHtml(htmlSampleLegacyVar);
final WebTemplate indexHtml = WebTemplate(htmlSampleLegacyVar);
indexHtml.applySubstitutions(
baseHref: '/foo/333/',
serviceWorkerVersion: 'v123xyz',
flutterJsFile: flutterJs,
);
expect(
indexHtml.content,
......@@ -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', () {
final IndexHtml indexHtml = IndexHtml(htmlSample2);
final WebTemplate indexHtml = WebTemplate(htmlSample2);
expect(indexHtml.getBaseHref(), ''); // Placeholder base href.
indexHtml.applySubstitutions(
baseHref: '/foo/333/',
serviceWorkerVersion: 'v123xyz',
flutterJsFile: flutterJs,
);
// The parsed base href should be updated after substitutions.
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 {
await _testProject(HotReloadProject(indexHtml: indexHtmlFlutterJsPromisesShort), name: 'flutter.js (promises, short)');
await _testProject(HotReloadProject(indexHtml: indexHtmlFlutterJsLoad), name: 'flutter.js (load)');
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 {
......@@ -73,8 +75,7 @@ Future<void> _testProject(HotReloadProject project, {String name = 'Default'}) a
completer.complete();
}
});
await flutter.run(chrome: true,
additionalCommandArgs: <String>['--dart-define=FLUTTER_WEB_USE_SKIA=true', '--verbose']);
await flutter.run(chrome: true, additionalCommandArgs: <String>['--verbose', '--web-renderer=canvaskit']);
project.uncommentHotReloadPrint();
try {
await flutter.hotRestart();
......
......@@ -198,3 +198,53 @@ $initScript
</body>
</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