[flutter_tools][web] Add support for web app manifests and arbitrary resource files (from web/) (#48316)
diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart
index adcbc2f..5fb6664 100644
--- a/dev/bots/analyze.dart
+++ b/dev/bots/analyze.dart
@@ -761,6 +761,13 @@
// (also used by a few examples)
Hash256(0xD29D4E0AF9256DC9, 0x2D0A8F8810608A5E, 0x64A132AD8B397CA2, 0xC4DDC0B1C26A68C3),
+ // packages/flutter_tools/templates/app/web/icons/Icon-192.png.copy.tmpl
+ // examples/flutter_gallery/web/icons/Icon-192.png
+ Hash256(0x3DCE99077602F704, 0x21C1C6B2A240BC9B, 0x83D64D86681D45F2, 0x154143310C980BE3),
+
+ // packages/flutter_tools/templates/app/web/icons/Icon-512.png.copy.tmpl
+ // examples/flutter_gallery/web/icons/Icon-512.png
+ Hash256(0xBACCB205AE45f0B4, 0x21BE1657259B4943, 0xAC40C95094AB877F, 0x3BCBE12CD544DCBE),
// GALLERY ICONS
@@ -994,7 +1001,7 @@
assert(
_grandfatheredBinaries
.expand<int>((Hash256 hash) => <int>[hash.a, hash.b, hash.c, hash.d])
- .reduce((int value, int element) => value ^ element) == 0x39A050CD69434936 // Please do not modify this line.
+ .reduce((int value, int element) => value ^ element) == 0xBFC18DE113B5AE8E // Please do not modify this line.
);
grandfatheredBinaries ??= _grandfatheredBinaries;
if (!Platform.isWindows) { // TODO(ianh): Port this to Windows
diff --git a/examples/flutter_gallery/web/icons/Icon-192.png b/examples/flutter_gallery/web/icons/Icon-192.png
new file mode 100644
index 0000000..b749bfef
--- /dev/null
+++ b/examples/flutter_gallery/web/icons/Icon-192.png
Binary files differ
diff --git a/examples/flutter_gallery/web/icons/Icon-512.png b/examples/flutter_gallery/web/icons/Icon-512.png
new file mode 100644
index 0000000..88cfd48
--- /dev/null
+++ b/examples/flutter_gallery/web/icons/Icon-512.png
Binary files differ
diff --git a/examples/flutter_gallery/web/index.html b/examples/flutter_gallery/web/index.html
index ce6634c..69b9962 100644
--- a/examples/flutter_gallery/web/index.html
+++ b/examples/flutter_gallery/web/index.html
@@ -3,10 +3,21 @@
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<html>
- <head>
- <title>Flutter Gallery</title>
- </head>
- <body>
- <script src="main.dart.js"></script>
- </body>
+<head>
+ <meta charset="UTF-8">
+ <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+ <meta name="description" content="A demo app for Flutter's material design and cupertino widgets, as well as many other features of the Flutter SDK.">
+
+ <!-- iOS meta tags & icons -->
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="apple-mobile-web-status-bar-style" content="black">
+ <meta name="apple-mobile-web-app-title" content="Flutter Gallery">
+ <link rel="apple-touch-icon" href="/icons/Icon-192.png">
+
+ <title>Flutter Gallery</title>
+ <link rel="manifest" href="/manifest.json">
+</head>
+<body>
+ <script src="main.dart.js" type="application/javascript"></script>
+</body>
</html>
diff --git a/examples/flutter_gallery/web/manifest.json b/examples/flutter_gallery/web/manifest.json
new file mode 100644
index 0000000..47be1a1
--- /dev/null
+++ b/examples/flutter_gallery/web/manifest.json
@@ -0,0 +1,23 @@
+{
+ "name": "flutter_gallery",
+ "short_name": "flutter_gallery",
+ "start_url": ".",
+ "display": "minimal-ui",
+ "background_color": "#0175C2",
+ "theme_color": "#0175C2",
+ "description": "A new Flutter project.",
+ "orientation": "portrait-primary",
+ "prefer_related_applications": false,
+ "icons": [
+ {
+ "src": "icons/Icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ]
+}
diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart
index 2237175..5b5f2b8 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/web.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart
@@ -198,7 +198,7 @@
}
}
-/// Unpacks the dart2js compilation to a given output directory
+/// Unpacks the dart2js compilation and resources to a given output directory
class WebReleaseBundle extends Target {
const WebReleaseBundle();
@@ -214,18 +214,18 @@
List<Source> get inputs => const <Source>[
Source.pattern('{BUILD_DIR}/main.dart.js'),
Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
- Source.pattern('{PROJECT_DIR}/web/index.html'),
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('{OUTPUT_DIR}/main.dart.js'),
- Source.pattern('{OUTPUT_DIR}/index.html'),
];
@override
List<String> get depfiles => const <String>[
'dart2js.d',
+ 'flutter_assets.d',
+ 'web_resources.d',
];
@override
@@ -240,11 +240,30 @@
}
final Directory outputDirectory = environment.outputDir.childDirectory('assets');
outputDirectory.createSync(recursive: true);
- environment.projectDir
- .childDirectory('web')
- .childFile('index.html')
- .copySync(globals.fs.path.join(environment.outputDir.path, 'index.html'));
final Depfile depfile = await copyAssets(environment, environment.outputDir.childDirectory('assets'));
depfile.writeToFile(environment.buildDir.childFile('flutter_assets.d'));
+
+ final Directory webResources = environment.projectDir
+ .childDirectory('web');
+ final List<File> inputResourceFiles = webResources
+ .listSync(recursive: true)
+ .whereType<File>()
+ .toList();
+
+ // Copy other resource files out of web/ directory.
+ final List<File> outputResourcesFiles = <File>[];
+ for (final File inputFile in inputResourceFiles) {
+ final File outputFile = globals.fs.file(globals.fs.path.join(
+ environment.outputDir.path,
+ globals.fs.path.relative(inputFile.path, from: webResources.path)));
+ if (!outputFile.parent.existsSync()) {
+ outputFile.parent.createSync(recursive: true);
+ }
+ inputFile.copySync(outputFile.path);
+ outputResourcesFiles.add(outputFile);
+ }
+ final Depfile resourceFile = Depfile(inputResourceFiles, outputResourcesFiles);
+ resourceFile.writeToFile(environment.buildDir.childFile('web_resources.d'));
+
}
}
diff --git a/packages/flutter_tools/templates/app/web/icons/Icon-192.png.copy.tmpl b/packages/flutter_tools/templates/app/web/icons/Icon-192.png.copy.tmpl
new file mode 100644
index 0000000..b749bfef
--- /dev/null
+++ b/packages/flutter_tools/templates/app/web/icons/Icon-192.png.copy.tmpl
Binary files differ
diff --git a/packages/flutter_tools/templates/app/web/icons/Icon-512.png.copy.tmpl b/packages/flutter_tools/templates/app/web/icons/Icon-512.png.copy.tmpl
new file mode 100644
index 0000000..88cfd48
--- /dev/null
+++ b/packages/flutter_tools/templates/app/web/icons/Icon-512.png.copy.tmpl
Binary files differ
diff --git a/packages/flutter_tools/templates/app/web/index.html.tmpl b/packages/flutter_tools/templates/app/web/index.html.tmpl
index 34621ea..dc50861 100644
--- a/packages/flutter_tools/templates/app/web/index.html.tmpl
+++ b/packages/flutter_tools/templates/app/web/index.html.tmpl
@@ -2,7 +2,17 @@
<html>
<head>
<meta charset="UTF-8">
+ <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+ <meta name="description" content="{{description}}">
+
+ <!-- iOS meta tags & icons -->
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="apple-mobile-web-status-bar-style" content="black">
+ <meta name="apple-mobile-web-app-title" content="{{projectName}}">
+ <link rel="apple-touch-icon" href="/icons/Icon-192.png">
+
<title>{{projectName}}</title>
+ <link rel="manifest" href="/manifest.json">
</head>
<body>
<script src="main.dart.js" type="application/javascript"></script>
diff --git a/packages/flutter_tools/templates/app/web/manifest.json.tmpl b/packages/flutter_tools/templates/app/web/manifest.json.tmpl
new file mode 100644
index 0000000..fb18a4d
--- /dev/null
+++ b/packages/flutter_tools/templates/app/web/manifest.json.tmpl
@@ -0,0 +1,23 @@
+{
+ "name": "{{projectName}}",
+ "short_name": "{{projectName}}",
+ "start_url": ".",
+ "display": "minimal-ui",
+ "background_color": "#0175C2",
+ "theme_color": "#0175C2",
+ "description": "{{description}}",
+ "orientation": "portrait-primary",
+ "prefer_related_applications": false,
+ "icons": [
+ {
+ "src": "icons/Icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ]
+}
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart
index b5c3b38..f3458b1 100644
--- a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart
@@ -31,6 +31,7 @@
when(mockPlatform.isWindows).thenReturn(false);
when(mockPlatform.isMacOS).thenReturn(true);
when(mockPlatform.isLinux).thenReturn(false);
+ when(mockPlatform.environment).thenReturn(const <String, String>{});
when(mockWindowsPlatform.isWindows).thenReturn(true);
when(mockWindowsPlatform.isMacOS).thenReturn(false);
@@ -41,10 +42,11 @@
..createSync(recursive: true)
..writeAsStringSync('foo:lib/\n');
PackageMap.globalPackagesPath = packagesFile.path;
+ globals.fs.currentDirectory.childDirectory('bar').createSync();
environment = Environment(
projectDir: globals.fs.currentDirectory.childDirectory('foo'),
- outputDir: globals.fs.currentDirectory,
+ outputDir: globals.fs.currentDirectory.childDirectory('bar'),
buildDir: globals.fs.currentDirectory,
defines: <String, String>{
kTargetFile: globals.fs.path.join('foo', 'lib', 'main.dart'),
@@ -77,6 +79,32 @@
expect(generated, contains("import 'package:foo/main.dart' as entrypoint;"));
}));
+ test('WebReleaseBundle copies dart2js output and resource files to output directory', () => testbed.run(() async {
+ final Directory webResources = environment.projectDir.childDirectory('web');
+ webResources.childFile('index.html')
+ ..createSync(recursive: true);
+ webResources.childFile('foo.txt')
+ ..writeAsStringSync('A');
+ environment.buildDir.childFile('main.dart.js').createSync();
+
+ await const WebReleaseBundle().build(environment);
+
+ expect(environment.outputDir.childFile('foo.txt')
+ .readAsStringSync(), 'A');
+ expect(environment.outputDir.childFile('main.dart.js')
+ .existsSync(), true);
+ expect(environment.outputDir.childDirectory('assets')
+ .childFile('AssetManifest.json').existsSync(), true);
+
+ // Update to arbitary resource file triggers rebuild.
+ webResources.childFile('foo.txt').writeAsStringSync('B');
+
+ await const WebReleaseBundle().build(environment);
+
+ expect(environment.outputDir.childFile('foo.txt')
+ .readAsStringSync(), 'B');
+ }));
+
test('WebEntrypointTarget generates an entrypoint for a file outside of main', () => testbed.run(() async {
environment.defines[kTargetFile] = globals.fs.path.join('other', 'lib', 'main.dart');
await const WebEntrypointTarget().build(environment);