[tools][web] Make Plugin Registrant file ephemeral. (#102185)
This commit is contained in:
parent
40627e9ed0
commit
1af8cc1183
@ -231,7 +231,6 @@ Future<void> runWebServiceWorkerTest({
|
||||
'main.dart.js': 1,
|
||||
'flutter_service_worker.js': 1,
|
||||
'assets/FontManifest.json': 1,
|
||||
'assets/NOTICES': 1,
|
||||
'assets/AssetManifest.json': 1,
|
||||
'CLOSE': 1,
|
||||
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'.
|
||||
@ -273,7 +272,6 @@ Future<void> runWebServiceWorkerTest({
|
||||
'flutter.js': 1,
|
||||
'flutter_service_worker.js': 2,
|
||||
'main.dart.js': 1,
|
||||
'assets/NOTICES': 1,
|
||||
'assets/AssetManifest.json': 1,
|
||||
'assets/FontManifest.json': 1,
|
||||
'CLOSE': 1,
|
||||
@ -305,7 +303,6 @@ Future<void> runWebServiceWorkerTest({
|
||||
'main.dart.js': 2,
|
||||
'assets/FontManifest.json': 2,
|
||||
'flutter_service_worker.js': 1,
|
||||
'assets/NOTICES': 1,
|
||||
'assets/AssetManifest.json': 1,
|
||||
'CLOSE': 1,
|
||||
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'.
|
||||
@ -358,7 +355,6 @@ Future<void> runWebServiceWorkerTest({
|
||||
'flutter.js': 1,
|
||||
'flutter_service_worker.js': 2,
|
||||
'main.dart.js': 2,
|
||||
'assets/NOTICES': 1,
|
||||
'assets/AssetManifest.json': 1,
|
||||
'assets/FontManifest.json': 2,
|
||||
'CLOSE': 1,
|
||||
|
@ -15,9 +15,12 @@ import '../../cache.dart';
|
||||
import '../../convert.dart';
|
||||
import '../../dart/language_version.dart';
|
||||
import '../../dart/package_map.dart';
|
||||
import '../../flutter_plugins.dart';
|
||||
import '../../globals.dart' as globals;
|
||||
import '../../project.dart';
|
||||
import '../../web/flutter_js.dart' as flutter_js;
|
||||
import '../../web/file_generators/flutter_js.dart' as flutter_js;
|
||||
import '../../web/file_generators/flutter_service_worker_js.dart';
|
||||
import '../../web/file_generators/main_dart.dart' as main_dart;
|
||||
import '../build_system.dart';
|
||||
import '../depfile.dart';
|
||||
import '../exceptions.dart';
|
||||
@ -50,15 +53,6 @@ const String kSourceMapsEnabled = 'SourceMaps';
|
||||
/// Whether the dart2js native null assertions are enabled.
|
||||
const String kNativeNullAssertions = 'NativeNullAssertions';
|
||||
|
||||
/// The caching strategy for the generated service worker.
|
||||
enum ServiceWorkerStrategy {
|
||||
/// Download the app shell eagerly and all other assets lazily.
|
||||
/// Prefer the offline cached version.
|
||||
offlineFirst,
|
||||
/// Do not generate a service worker,
|
||||
none,
|
||||
}
|
||||
|
||||
const String kOfflineFirst = 'offline-first';
|
||||
const String kNoneWorker = 'none';
|
||||
|
||||
@ -97,7 +91,6 @@ class WebEntrypointTarget extends Target {
|
||||
@override
|
||||
Future<void> build(Environment environment) async {
|
||||
final String? targetFile = environment.defines[kTargetFile];
|
||||
final bool hasWebPlugins = environment.defines[kHasWebPlugins] == 'true';
|
||||
final Uri importUri = environment.fileSystem.file(targetFile).absolute.uri;
|
||||
// TODO(zanderso): support configuration of this file.
|
||||
const String packageFile = '.packages';
|
||||
@ -124,50 +117,15 @@ class WebEntrypointTarget extends Target {
|
||||
final String importedEntrypoint = packageConfig.toPackageUri(importUri)?.toString()
|
||||
?? importUri.toString();
|
||||
|
||||
String? generatedImport;
|
||||
if (hasWebPlugins) {
|
||||
final Uri generatedUri = environment.projectDir
|
||||
.childDirectory('lib')
|
||||
.childFile('generated_plugin_registrant.dart')
|
||||
.absolute
|
||||
.uri;
|
||||
generatedImport = packageConfig.toPackageUri(generatedUri)?.toString()
|
||||
?? generatedUri.toString();
|
||||
}
|
||||
await injectBuildTimePluginFiles(flutterProject, webPlatform: true, destination: environment.buildDir);
|
||||
// The below works because `injectBuildTimePluginFiles` is configured to write
|
||||
// the web_plugin_registrant.dart file alongside the generated main.dart
|
||||
const String generatedImport = 'web_plugin_registrant.dart';
|
||||
|
||||
final String contents = <String>[
|
||||
'// @dart=${languageVersion.major}.${languageVersion.minor}',
|
||||
'// Flutter web bootstrap script for $importedEntrypoint.',
|
||||
'',
|
||||
"import 'dart:ui' as ui;",
|
||||
"import 'dart:async';",
|
||||
'',
|
||||
"import '$importedEntrypoint' as entrypoint;",
|
||||
if (hasWebPlugins)
|
||||
"import 'package:flutter_web_plugins/flutter_web_plugins.dart';",
|
||||
if (hasWebPlugins)
|
||||
"import '$generatedImport';",
|
||||
'',
|
||||
'typedef _UnaryFunction = dynamic Function(List<String> args);',
|
||||
'typedef _NullaryFunction = dynamic Function();',
|
||||
'',
|
||||
'Future<void> main() async {',
|
||||
' await ui.webOnlyWarmupEngine(',
|
||||
' runApp: () {',
|
||||
' if (entrypoint.main is _UnaryFunction) {',
|
||||
' return (entrypoint.main as _UnaryFunction)(<String>[]);',
|
||||
' }',
|
||||
' return (entrypoint.main as _NullaryFunction)();',
|
||||
' },',
|
||||
if (hasWebPlugins) ...<String>[
|
||||
' registerPlugins: () {',
|
||||
' registerPlugins(webPluginRegistrar);',
|
||||
' },',
|
||||
],
|
||||
' );',
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
final String contents = main_dart.generateMainDartFile(importedEntrypoint,
|
||||
languageVersion: languageVersion,
|
||||
pluginRegistrantEntrypoint: generatedImport,
|
||||
);
|
||||
|
||||
environment.buildDir.childFile('main.dart')
|
||||
.writeAsStringSync(contents);
|
||||
@ -547,7 +505,6 @@ class WebServiceWorker extends Target {
|
||||
<String>[
|
||||
'main.dart.js',
|
||||
'index.html',
|
||||
'assets/NOTICES',
|
||||
if (urlToHash.containsKey('assets/AssetManifest.json'))
|
||||
'assets/AssetManifest.json',
|
||||
if (urlToHash.containsKey('assets/FontManifest.json'))
|
||||
@ -567,194 +524,3 @@ class WebServiceWorker extends Target {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a service worker with an app-specific cache name a map of
|
||||
/// resource files.
|
||||
///
|
||||
/// The tool embeds file hashes directly into the worker so that the byte for byte
|
||||
/// invalidation will automatically reactivate workers whenever a new
|
||||
/// version is deployed.
|
||||
String generateServiceWorker(
|
||||
Map<String, String> resources,
|
||||
List<String> coreBundle, {
|
||||
required ServiceWorkerStrategy serviceWorkerStrategy,
|
||||
}) {
|
||||
if (serviceWorkerStrategy == ServiceWorkerStrategy.none) {
|
||||
return '';
|
||||
}
|
||||
return '''
|
||||
'use strict';
|
||||
const MANIFEST = 'flutter-app-manifest';
|
||||
const TEMP = 'flutter-temp-cache';
|
||||
const CACHE_NAME = 'flutter-app-cache';
|
||||
const RESOURCES = {
|
||||
${resources.entries.map((MapEntry<String, String> entry) => '"${entry.key}": "${entry.value}"').join(",\n")}
|
||||
};
|
||||
|
||||
// The application shell files that are downloaded before a service worker can
|
||||
// start.
|
||||
const CORE = [
|
||||
${coreBundle.map((String file) => '"$file"').join(',\n')}];
|
||||
// During install, the TEMP cache is populated with the application shell files.
|
||||
self.addEventListener("install", (event) => {
|
||||
self.skipWaiting();
|
||||
return event.waitUntil(
|
||||
caches.open(TEMP).then((cache) => {
|
||||
return cache.addAll(
|
||||
CORE.map((value) => new Request(value, {'cache': 'reload'})));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// During activate, the cache is populated with the temp files downloaded in
|
||||
// install. If this service worker is upgrading from one with a saved
|
||||
// MANIFEST, then use this to retain unchanged resource files.
|
||||
self.addEventListener("activate", function(event) {
|
||||
return event.waitUntil(async function() {
|
||||
try {
|
||||
var contentCache = await caches.open(CACHE_NAME);
|
||||
var tempCache = await caches.open(TEMP);
|
||||
var manifestCache = await caches.open(MANIFEST);
|
||||
var manifest = await manifestCache.match('manifest');
|
||||
// When there is no prior manifest, clear the entire cache.
|
||||
if (!manifest) {
|
||||
await caches.delete(CACHE_NAME);
|
||||
contentCache = await caches.open(CACHE_NAME);
|
||||
for (var request of await tempCache.keys()) {
|
||||
var response = await tempCache.match(request);
|
||||
await contentCache.put(request, response);
|
||||
}
|
||||
await caches.delete(TEMP);
|
||||
// Save the manifest to make future upgrades efficient.
|
||||
await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES)));
|
||||
return;
|
||||
}
|
||||
var oldManifest = await manifest.json();
|
||||
var origin = self.location.origin;
|
||||
for (var request of await contentCache.keys()) {
|
||||
var key = request.url.substring(origin.length + 1);
|
||||
if (key == "") {
|
||||
key = "/";
|
||||
}
|
||||
// If a resource from the old manifest is not in the new cache, or if
|
||||
// the MD5 sum has changed, delete it. Otherwise the resource is left
|
||||
// in the cache and can be reused by the new service worker.
|
||||
if (!RESOURCES[key] || RESOURCES[key] != oldManifest[key]) {
|
||||
await contentCache.delete(request);
|
||||
}
|
||||
}
|
||||
// Populate the cache with the app shell TEMP files, potentially overwriting
|
||||
// cache files preserved above.
|
||||
for (var request of await tempCache.keys()) {
|
||||
var response = await tempCache.match(request);
|
||||
await contentCache.put(request, response);
|
||||
}
|
||||
await caches.delete(TEMP);
|
||||
// Save the manifest to make future upgrades efficient.
|
||||
await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES)));
|
||||
return;
|
||||
} catch (err) {
|
||||
// On an unhandled exception the state of the cache cannot be guaranteed.
|
||||
console.error('Failed to upgrade service worker: ' + err);
|
||||
await caches.delete(CACHE_NAME);
|
||||
await caches.delete(TEMP);
|
||||
await caches.delete(MANIFEST);
|
||||
}
|
||||
}());
|
||||
});
|
||||
|
||||
// The fetch handler redirects requests for RESOURCE files to the service
|
||||
// worker cache.
|
||||
self.addEventListener("fetch", (event) => {
|
||||
if (event.request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
var origin = self.location.origin;
|
||||
var key = event.request.url.substring(origin.length + 1);
|
||||
// Redirect URLs to the index.html
|
||||
if (key.indexOf('?v=') != -1) {
|
||||
key = key.split('?v=')[0];
|
||||
}
|
||||
if (event.request.url == origin || event.request.url.startsWith(origin + '/#') || key == '') {
|
||||
key = '/';
|
||||
}
|
||||
// If the URL is not the RESOURCE list then return to signal that the
|
||||
// browser should take over.
|
||||
if (!RESOURCES[key]) {
|
||||
return;
|
||||
}
|
||||
// If the URL is the index.html, perform an online-first request.
|
||||
if (key == '/') {
|
||||
return onlineFirst(event);
|
||||
}
|
||||
event.respondWith(caches.open(CACHE_NAME)
|
||||
.then((cache) => {
|
||||
return cache.match(event.request).then((response) => {
|
||||
// Either respond with the cached resource, or perform a fetch and
|
||||
// lazily populate the cache.
|
||||
return response || fetch(event.request).then((response) => {
|
||||
cache.put(event.request, response.clone());
|
||||
return response;
|
||||
});
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
// SkipWaiting can be used to immediately activate a waiting service worker.
|
||||
// This will also require a page refresh triggered by the main worker.
|
||||
if (event.data === 'skipWaiting') {
|
||||
self.skipWaiting();
|
||||
return;
|
||||
}
|
||||
if (event.data === 'downloadOffline') {
|
||||
downloadOffline();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Download offline will check the RESOURCES for all files not in the cache
|
||||
// and populate them.
|
||||
async function downloadOffline() {
|
||||
var resources = [];
|
||||
var contentCache = await caches.open(CACHE_NAME);
|
||||
var currentContent = {};
|
||||
for (var request of await contentCache.keys()) {
|
||||
var key = request.url.substring(origin.length + 1);
|
||||
if (key == "") {
|
||||
key = "/";
|
||||
}
|
||||
currentContent[key] = true;
|
||||
}
|
||||
for (var resourceKey of Object.keys(RESOURCES)) {
|
||||
if (!currentContent[resourceKey]) {
|
||||
resources.push(resourceKey);
|
||||
}
|
||||
}
|
||||
return contentCache.addAll(resources);
|
||||
}
|
||||
|
||||
// Attempt to download the resource online before falling back to
|
||||
// the offline cache.
|
||||
function onlineFirst(event) {
|
||||
return event.respondWith(
|
||||
fetch(event.request).then((response) => {
|
||||
return caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(event.request, response.clone());
|
||||
return response;
|
||||
});
|
||||
}).catch((error) => {
|
||||
return caches.open(CACHE_NAME).then((cache) => {
|
||||
return cache.match(event.request).then((response) => {
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
''';
|
||||
}
|
||||
|
@ -550,23 +550,32 @@ Depends on all your plugins, and provides a function to register them.
|
||||
end
|
||||
''';
|
||||
|
||||
const String _dartPluginRegistryTemplate = '''
|
||||
const String _noopDartPluginRegistryTemplate = '''
|
||||
// Flutter web plugin registrant file.
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// ignore_for_file: directives_ordering
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
// ignore_for_file: depend_on_referenced_packages
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
void registerPlugins() {}
|
||||
''';
|
||||
|
||||
const String _dartPluginRegistryTemplate = '''
|
||||
// Flutter web plugin registrant file.
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
{{#methodChannelPlugins}}
|
||||
import 'package:{{name}}/{{file}}';
|
||||
{{/methodChannelPlugins}}
|
||||
|
||||
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||
|
||||
// ignore: public_member_api_docs
|
||||
void registerPlugins(final Registrar registrar) {
|
||||
void registerPlugins([final Registrar? pluginRegistrar]) {
|
||||
final Registrar registrar = pluginRegistrar ?? webPluginRegistrar;
|
||||
{{#methodChannelPlugins}}
|
||||
{{class}}.registerWith(registrar);
|
||||
{{/methodChannelPlugins}}
|
||||
@ -937,22 +946,22 @@ Future<void> _writeCppPluginRegistrant(Directory destination, Map<String, Object
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
|
||||
Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plugins, Directory destination) async {
|
||||
final List<Map<String, Object?>> webPlugins = _extractPlatformMaps(plugins, WebPlugin.kConfigKey);
|
||||
final Map<String, Object> context = <String, Object>{
|
||||
'methodChannelPlugins': webPlugins,
|
||||
};
|
||||
final File pluginFile = project.web.libDirectory.childFile('generated_plugin_registrant.dart');
|
||||
if (webPlugins.isEmpty) {
|
||||
ErrorHandlingFileSystem.deleteIfExists(pluginFile);
|
||||
} else {
|
||||
_renderTemplateToFile(
|
||||
_dartPluginRegistryTemplate,
|
||||
context,
|
||||
pluginFile,
|
||||
globals.templateRenderer,
|
||||
);
|
||||
}
|
||||
|
||||
final File pluginFile = destination.childFile('web_plugin_registrant.dart');
|
||||
|
||||
final String template = webPlugins.isEmpty ? _noopDartPluginRegistryTemplate : _dartPluginRegistryTemplate;
|
||||
|
||||
_renderTemplateToFile(
|
||||
template,
|
||||
context,
|
||||
pluginFile,
|
||||
globals.templateRenderer,
|
||||
);
|
||||
}
|
||||
|
||||
/// For each platform that uses them, creates symlinks within the platform
|
||||
@ -1068,8 +1077,41 @@ Future<void> refreshPluginsList(
|
||||
}
|
||||
}
|
||||
|
||||
/// Injects plugins found in `pubspec.yaml` into the platform-specific projects
|
||||
/// only at build-time.
|
||||
///
|
||||
/// This method is similar to [injectPlugins], but used only for platforms where
|
||||
/// the plugin files are not required when the app is created (currently: Web).
|
||||
///
|
||||
/// This method will create files in the temporary flutter build directory
|
||||
/// specified by `destination`.
|
||||
///
|
||||
/// In the Web platform, `destination` can point to a real filesystem (`flutter build`)
|
||||
/// or an in-memory filesystem (`flutter run`).
|
||||
Future<void> injectBuildTimePluginFiles(
|
||||
FlutterProject project, {
|
||||
required Directory destination,
|
||||
bool webPlatform = false,
|
||||
}) async {
|
||||
final List<Plugin> plugins = await findPlugins(project);
|
||||
// Sort the plugins by name to keep ordering stable in generated files.
|
||||
plugins.sort((Plugin left, Plugin right) => left.name.compareTo(right.name));
|
||||
if (webPlatform) {
|
||||
await _writeWebPluginRegistrant(project, plugins, destination);
|
||||
}
|
||||
}
|
||||
|
||||
/// Injects plugins found in `pubspec.yaml` into the platform-specific projects.
|
||||
///
|
||||
/// The injected files are required by the flutter app as soon as possible, so
|
||||
/// it can be built.
|
||||
///
|
||||
/// Files written by this method end up in platform-specific locations that are
|
||||
/// configured by each [FlutterProject] subclass (except for the Web).
|
||||
///
|
||||
/// Web tooling uses [injectBuildTimePluginFiles] instead, which places files in the
|
||||
/// current build (temp) directory, and doesn't modify the users' working copy.
|
||||
///
|
||||
/// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`.
|
||||
Future<void> injectPlugins(
|
||||
FlutterProject project, {
|
||||
@ -1078,7 +1120,6 @@ Future<void> injectPlugins(
|
||||
bool linuxPlatform = false,
|
||||
bool macOSPlatform = false,
|
||||
bool windowsPlatform = false,
|
||||
bool webPlatform = false,
|
||||
}) async {
|
||||
final List<Plugin> plugins = await findPlugins(project);
|
||||
// Sort the plugins by name to keep ordering stable in generated files.
|
||||
@ -1114,9 +1155,6 @@ Future<void> injectPlugins(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (webPlatform) {
|
||||
await _writeWebPluginRegistrant(project, plugins);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the specified Flutter [project] has any plugin dependencies.
|
||||
|
@ -41,7 +41,7 @@ import '../vmservice.dart';
|
||||
import '../web/bootstrap.dart';
|
||||
import '../web/chrome.dart';
|
||||
import '../web/compile.dart';
|
||||
import '../web/flutter_js.dart' as flutter_js;
|
||||
import '../web/file_generators/flutter_js.dart' as flutter_js;
|
||||
import '../web/memory_fs.dart';
|
||||
import 'sdk_web_configuration.dart';
|
||||
|
||||
|
@ -30,8 +30,6 @@ import '../dart/language_version.dart';
|
||||
import '../devfs.dart';
|
||||
import '../device.dart';
|
||||
import '../flutter_plugins.dart';
|
||||
import '../platform_plugins.dart';
|
||||
import '../plugins.dart';
|
||||
import '../project.dart';
|
||||
import '../reporting/reporting.dart';
|
||||
import '../resident_devtools_handler.dart';
|
||||
@ -40,6 +38,7 @@ import '../run_hot.dart';
|
||||
import '../vmservice.dart';
|
||||
import '../web/chrome.dart';
|
||||
import '../web/compile.dart';
|
||||
import '../web/file_generators/main_dart.dart' as main_dart;
|
||||
import '../web/web_device.dart';
|
||||
import '../web/web_runner.dart';
|
||||
import 'devfs_web.dart';
|
||||
@ -433,15 +432,12 @@ class ResidentWebRunner extends ResidentRunner {
|
||||
..createSync();
|
||||
result = _generatedEntrypointDirectory.childFile('web_entrypoint.dart');
|
||||
|
||||
final bool hasWebPlugins = (await findPlugins(flutterProject))
|
||||
.any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey));
|
||||
await injectPlugins(flutterProject, webPlatform: true);
|
||||
// Generates the generated_plugin_registrar
|
||||
await injectBuildTimePluginFiles(flutterProject, webPlatform: true, destination: _generatedEntrypointDirectory);
|
||||
// The below works because `injectBuildTimePluginFiles` is configured to write
|
||||
// the web_plugin_registrant.dart file alongside the generated main.dart
|
||||
const String/*?*/ generatedImport = 'web_plugin_registrant.dart';
|
||||
|
||||
final Uri generatedUri = _fileSystem.currentDirectory
|
||||
.childDirectory('lib')
|
||||
.childFile('generated_plugin_registrant.dart')
|
||||
.absolute.uri;
|
||||
final Uri generatedImport = packageConfig.toPackageUri(generatedUri);
|
||||
Uri importedEntrypoint = packageConfig.toPackageUri(mainUri);
|
||||
// Special handling for entrypoints that are not under lib, such as test scripts.
|
||||
if (importedEntrypoint == null) {
|
||||
@ -453,44 +449,17 @@ class ResidentWebRunner extends ResidentRunner {
|
||||
path: '/${mainUri.pathSegments.last}',
|
||||
);
|
||||
}
|
||||
final LanguageVersion languageVersion = determineLanguageVersion(
|
||||
final LanguageVersion languageVersion = determineLanguageVersion(
|
||||
_fileSystem.file(mainUri),
|
||||
packageConfig[flutterProject.manifest.appName],
|
||||
Cache.flutterRoot,
|
||||
);
|
||||
|
||||
final String entrypoint = <String>[
|
||||
'// @dart=${languageVersion.major}.${languageVersion.minor}',
|
||||
'// Flutter web bootstrap script for $importedEntrypoint.',
|
||||
'',
|
||||
"import 'dart:ui' as ui;",
|
||||
"import 'dart:async';",
|
||||
'',
|
||||
"import '$importedEntrypoint' as entrypoint;",
|
||||
if (hasWebPlugins)
|
||||
"import 'package:flutter_web_plugins/flutter_web_plugins.dart';",
|
||||
if (hasWebPlugins)
|
||||
"import '$generatedImport';",
|
||||
'',
|
||||
'typedef _UnaryFunction = dynamic Function(List<String> args);',
|
||||
'typedef _NullaryFunction = dynamic Function();',
|
||||
'Future<void> main() async {',
|
||||
' await ui.webOnlyWarmupEngine(',
|
||||
' runApp: () {',
|
||||
' if (entrypoint.main is _UnaryFunction) {',
|
||||
' return (entrypoint.main as _UnaryFunction)(<String>[]);',
|
||||
' }',
|
||||
' return (entrypoint.main as _NullaryFunction)();',
|
||||
' },',
|
||||
if (hasWebPlugins) ...<String>[
|
||||
' registerPlugins: () {',
|
||||
' registerPlugins(webPluginRegistrar);',
|
||||
' },',
|
||||
],
|
||||
' );',
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
final String entrypoint = main_dart.generateMainDartFile(importedEntrypoint.toString(),
|
||||
languageVersion: languageVersion,
|
||||
pluginRegistrantEntrypoint: generatedImport,
|
||||
);
|
||||
|
||||
result.writeAsStringSync(entrypoint);
|
||||
}
|
||||
return result.absolute.uri;
|
||||
|
@ -373,7 +373,6 @@ class FlutterProject {
|
||||
linuxPlatform: linuxPlatform,
|
||||
macOSPlatform: macOSPlatform,
|
||||
windowsPlatform: windowsPlatform,
|
||||
webPlatform: webPlatform,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import '../artifacts.dart';
|
||||
import '../base/common.dart';
|
||||
import '../base/file_system.dart';
|
||||
import '../base/logger.dart';
|
||||
import '../base/project_migrator.dart';
|
||||
import '../build_info.dart';
|
||||
import '../build_system/build_system.dart';
|
||||
import '../build_system/targets/web.dart';
|
||||
@ -15,6 +16,7 @@ import '../globals.dart' as globals;
|
||||
import '../platform_plugins.dart';
|
||||
import '../plugins.dart';
|
||||
import '../project.dart';
|
||||
import 'migrations/scrub_generated_plugin_registrant.dart';
|
||||
|
||||
Future<void> buildWeb(
|
||||
FlutterProject flutterProject,
|
||||
@ -32,7 +34,16 @@ Future<void> buildWeb(
|
||||
final Directory outputDirectory = globals.fs.directory(getWebBuildDirectory());
|
||||
outputDirectory.createSync(recursive: true);
|
||||
|
||||
await injectPlugins(flutterProject, webPlatform: true);
|
||||
// The migrators to apply to a Web project.
|
||||
final List<ProjectMigrator> migrators = <ProjectMigrator>[
|
||||
ScrubGeneratedPluginRegistrant(flutterProject.web, globals.logger),
|
||||
];
|
||||
|
||||
final ProjectMigration migration = ProjectMigration(migrators);
|
||||
if (!migration.run()) {
|
||||
throwToolExit('Failed to run all web migrations.');
|
||||
}
|
||||
|
||||
final Status status = globals.logger.startProgress('Compiling $target for the Web...');
|
||||
final Stopwatch sw = Stopwatch()..start();
|
||||
try {
|
||||
|
@ -0,0 +1,203 @@
|
||||
// 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.
|
||||
|
||||
/// The caching strategy for the generated service worker.
|
||||
enum ServiceWorkerStrategy {
|
||||
/// Download the app shell eagerly and all other assets lazily.
|
||||
/// Prefer the offline cached version.
|
||||
offlineFirst,
|
||||
/// Do not generate a service worker,
|
||||
none,
|
||||
}
|
||||
|
||||
/// Generate a service worker with an app-specific cache name a map of
|
||||
/// resource files.
|
||||
///
|
||||
/// The tool embeds file hashes directly into the worker so that the byte for byte
|
||||
/// invalidation will automatically reactivate workers whenever a new
|
||||
/// version is deployed.
|
||||
String generateServiceWorker(
|
||||
Map<String, String> resources,
|
||||
List<String> coreBundle, {
|
||||
required ServiceWorkerStrategy serviceWorkerStrategy,
|
||||
}) {
|
||||
if (serviceWorkerStrategy == ServiceWorkerStrategy.none) {
|
||||
return '';
|
||||
}
|
||||
return '''
|
||||
'use strict';
|
||||
const MANIFEST = 'flutter-app-manifest';
|
||||
const TEMP = 'flutter-temp-cache';
|
||||
const CACHE_NAME = 'flutter-app-cache';
|
||||
const RESOURCES = {
|
||||
${resources.entries.map((MapEntry<String, String> entry) => '"${entry.key}": "${entry.value}"').join(",\n")}
|
||||
};
|
||||
|
||||
// The application shell files that are downloaded before a service worker can
|
||||
// start.
|
||||
const CORE = [
|
||||
${coreBundle.map((String file) => '"$file"').join(',\n')}];
|
||||
// During install, the TEMP cache is populated with the application shell files.
|
||||
self.addEventListener("install", (event) => {
|
||||
self.skipWaiting();
|
||||
return event.waitUntil(
|
||||
caches.open(TEMP).then((cache) => {
|
||||
return cache.addAll(
|
||||
CORE.map((value) => new Request(value, {'cache': 'reload'})));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// During activate, the cache is populated with the temp files downloaded in
|
||||
// install. If this service worker is upgrading from one with a saved
|
||||
// MANIFEST, then use this to retain unchanged resource files.
|
||||
self.addEventListener("activate", function(event) {
|
||||
return event.waitUntil(async function() {
|
||||
try {
|
||||
var contentCache = await caches.open(CACHE_NAME);
|
||||
var tempCache = await caches.open(TEMP);
|
||||
var manifestCache = await caches.open(MANIFEST);
|
||||
var manifest = await manifestCache.match('manifest');
|
||||
// When there is no prior manifest, clear the entire cache.
|
||||
if (!manifest) {
|
||||
await caches.delete(CACHE_NAME);
|
||||
contentCache = await caches.open(CACHE_NAME);
|
||||
for (var request of await tempCache.keys()) {
|
||||
var response = await tempCache.match(request);
|
||||
await contentCache.put(request, response);
|
||||
}
|
||||
await caches.delete(TEMP);
|
||||
// Save the manifest to make future upgrades efficient.
|
||||
await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES)));
|
||||
return;
|
||||
}
|
||||
var oldManifest = await manifest.json();
|
||||
var origin = self.location.origin;
|
||||
for (var request of await contentCache.keys()) {
|
||||
var key = request.url.substring(origin.length + 1);
|
||||
if (key == "") {
|
||||
key = "/";
|
||||
}
|
||||
// If a resource from the old manifest is not in the new cache, or if
|
||||
// the MD5 sum has changed, delete it. Otherwise the resource is left
|
||||
// in the cache and can be reused by the new service worker.
|
||||
if (!RESOURCES[key] || RESOURCES[key] != oldManifest[key]) {
|
||||
await contentCache.delete(request);
|
||||
}
|
||||
}
|
||||
// Populate the cache with the app shell TEMP files, potentially overwriting
|
||||
// cache files preserved above.
|
||||
for (var request of await tempCache.keys()) {
|
||||
var response = await tempCache.match(request);
|
||||
await contentCache.put(request, response);
|
||||
}
|
||||
await caches.delete(TEMP);
|
||||
// Save the manifest to make future upgrades efficient.
|
||||
await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES)));
|
||||
return;
|
||||
} catch (err) {
|
||||
// On an unhandled exception the state of the cache cannot be guaranteed.
|
||||
console.error('Failed to upgrade service worker: ' + err);
|
||||
await caches.delete(CACHE_NAME);
|
||||
await caches.delete(TEMP);
|
||||
await caches.delete(MANIFEST);
|
||||
}
|
||||
}());
|
||||
});
|
||||
|
||||
// The fetch handler redirects requests for RESOURCE files to the service
|
||||
// worker cache.
|
||||
self.addEventListener("fetch", (event) => {
|
||||
if (event.request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
var origin = self.location.origin;
|
||||
var key = event.request.url.substring(origin.length + 1);
|
||||
// Redirect URLs to the index.html
|
||||
if (key.indexOf('?v=') != -1) {
|
||||
key = key.split('?v=')[0];
|
||||
}
|
||||
if (event.request.url == origin || event.request.url.startsWith(origin + '/#') || key == '') {
|
||||
key = '/';
|
||||
}
|
||||
// If the URL is not the RESOURCE list then return to signal that the
|
||||
// browser should take over.
|
||||
if (!RESOURCES[key]) {
|
||||
return;
|
||||
}
|
||||
// If the URL is the index.html, perform an online-first request.
|
||||
if (key == '/') {
|
||||
return onlineFirst(event);
|
||||
}
|
||||
event.respondWith(caches.open(CACHE_NAME)
|
||||
.then((cache) => {
|
||||
return cache.match(event.request).then((response) => {
|
||||
// Either respond with the cached resource, or perform a fetch and
|
||||
// lazily populate the cache.
|
||||
return response || fetch(event.request).then((response) => {
|
||||
cache.put(event.request, response.clone());
|
||||
return response;
|
||||
});
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
// SkipWaiting can be used to immediately activate a waiting service worker.
|
||||
// This will also require a page refresh triggered by the main worker.
|
||||
if (event.data === 'skipWaiting') {
|
||||
self.skipWaiting();
|
||||
return;
|
||||
}
|
||||
if (event.data === 'downloadOffline') {
|
||||
downloadOffline();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Download offline will check the RESOURCES for all files not in the cache
|
||||
// and populate them.
|
||||
async function downloadOffline() {
|
||||
var resources = [];
|
||||
var contentCache = await caches.open(CACHE_NAME);
|
||||
var currentContent = {};
|
||||
for (var request of await contentCache.keys()) {
|
||||
var key = request.url.substring(origin.length + 1);
|
||||
if (key == "") {
|
||||
key = "/";
|
||||
}
|
||||
currentContent[key] = true;
|
||||
}
|
||||
for (var resourceKey of Object.keys(RESOURCES)) {
|
||||
if (!currentContent[resourceKey]) {
|
||||
resources.push(resourceKey);
|
||||
}
|
||||
}
|
||||
return contentCache.addAll(resources);
|
||||
}
|
||||
|
||||
// Attempt to download the resource online before falling back to
|
||||
// the offline cache.
|
||||
function onlineFirst(event) {
|
||||
return event.respondWith(
|
||||
fetch(event.request).then((response) => {
|
||||
return caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(event.request, response.clone());
|
||||
return response;
|
||||
});
|
||||
}).catch((error) => {
|
||||
return caches.open(CACHE_NAME).then((cache) => {
|
||||
return cache.match(event.request).then((response) => {
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
''';
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
// 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 'package:package_config/package_config.dart';
|
||||
|
||||
/// Generates the main.dart file.
|
||||
String generateMainDartFile(String appEntrypoint, {
|
||||
required String pluginRegistrantEntrypoint,
|
||||
LanguageVersion? languageVersion,
|
||||
}) {
|
||||
return <String>[
|
||||
if (languageVersion != null)
|
||||
'// @dart=${languageVersion.major}.${languageVersion.minor}',
|
||||
'// Flutter web bootstrap script for $appEntrypoint.',
|
||||
'//',
|
||||
'// Generated file. Do not edit.',
|
||||
'//',
|
||||
'',
|
||||
'// ignore_for_file: type=lint',
|
||||
'',
|
||||
"import 'dart:ui' as ui;",
|
||||
"import 'dart:async';",
|
||||
'',
|
||||
"import '$appEntrypoint' as entrypoint;",
|
||||
"import '$pluginRegistrantEntrypoint' as pluginRegistrant;",
|
||||
'',
|
||||
'typedef _UnaryFunction = dynamic Function(List<String> args);',
|
||||
'typedef _NullaryFunction = dynamic Function();',
|
||||
'',
|
||||
'Future<void> main() async {',
|
||||
' await ui.webOnlyWarmupEngine(',
|
||||
' runApp: () {',
|
||||
' if (entrypoint.main is _UnaryFunction) {',
|
||||
' return (entrypoint.main as _UnaryFunction)(<String>[]);',
|
||||
' }',
|
||||
' return (entrypoint.main as _NullaryFunction)();',
|
||||
' },',
|
||||
' registerPlugins: () {',
|
||||
' pluginRegistrant.registerPlugins();',
|
||||
' },',
|
||||
' );',
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
// 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 '../../base/file_system.dart';
|
||||
import '../../base/logger.dart';
|
||||
import '../../base/project_migrator.dart';
|
||||
import '../../project.dart';
|
||||
|
||||
/// Remove lib/generated_plugin_registrant.dart if it exists.
|
||||
class ScrubGeneratedPluginRegistrant extends ProjectMigrator {
|
||||
ScrubGeneratedPluginRegistrant(
|
||||
WebProject project,
|
||||
super.logger,
|
||||
) : _project = project, _logger = logger;
|
||||
|
||||
final WebProject _project;
|
||||
final Logger _logger;
|
||||
|
||||
@override
|
||||
bool migrate() {
|
||||
final File registrant = _project.libDirectory.childFile('generated_plugin_registrant.dart');
|
||||
final File gitignore = _project.parent.directory.childFile('.gitignore');
|
||||
|
||||
if (!removeFile(registrant)) {
|
||||
return false;
|
||||
}
|
||||
if (gitignore.existsSync()) {
|
||||
processFileLines(gitignore);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cleans up the .gitignore by removing the line that mentions generated_plugin_registrant.
|
||||
@override
|
||||
String? migrateLine(String line) {
|
||||
return line.contains('lib/generated_plugin_registrant.dart') ? null : line;
|
||||
}
|
||||
|
||||
bool removeFile(File file) {
|
||||
if (!file.existsSync()) {
|
||||
_logger.printTrace('${file.basename} not found. Skipping.');
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
file.deleteSync();
|
||||
_logger.printStatus('${file.basename} found. Deleted.');
|
||||
return true;
|
||||
} on FileSystemException catch (e, s) {
|
||||
_logger.printError(e.message, stackTrace: s);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -32,9 +32,6 @@ migrate_working_dir/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
|
@ -92,7 +92,9 @@ void main() {
|
||||
setupFileSystemForEndToEndTest(fileSystem);
|
||||
await runner.run(<String>['build', 'web', '--no-pub', '--dart-define=foo=a', '--dart2js-optimization=O3']);
|
||||
|
||||
expect(fileSystem.file(fileSystem.path.join('lib', 'generated_plugin_registrant.dart')).existsSync(), true);
|
||||
final Directory buildDir = fileSystem.directory(fileSystem.path.join('build', 'web'));
|
||||
|
||||
expect(buildDir.existsSync(), true);
|
||||
}, overrides: <Type, Generator>{
|
||||
Platform: () => fakePlatform,
|
||||
FileSystem: () => fileSystem,
|
||||
|
@ -8,12 +8,15 @@ import 'package:file_testing/file_testing.dart';
|
||||
import 'package:flutter_tools/src/artifacts.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/platform.dart';
|
||||
import 'package:flutter_tools/src/base/template.dart';
|
||||
import 'package:flutter_tools/src/build_info.dart';
|
||||
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/web/flutter_js.dart' as flutter_js;
|
||||
import 'package:flutter_tools/src/isolated/mustache_template.dart';
|
||||
import 'package:flutter_tools/src/web/file_generators/flutter_js.dart' as flutter_js;
|
||||
import 'package:flutter_tools/src/web/file_generators/flutter_service_worker_js.dart';
|
||||
|
||||
import '../../../src/common.dart';
|
||||
import '../../../src/fake_process_manager.dart';
|
||||
@ -80,8 +83,8 @@ void main() {
|
||||
final String generated = environment.buildDir.childFile('main.dart').readAsStringSync();
|
||||
|
||||
// Plugins
|
||||
expect(generated, contains("import 'package:foo/generated_plugin_registrant.dart';"));
|
||||
expect(generated, contains('registerPlugins(webPluginRegistrar);'));
|
||||
expect(generated, contains("import 'web_plugin_registrant.dart' as pluginRegistrant;"));
|
||||
expect(generated, contains('pluginRegistrant.registerPlugins();'));
|
||||
|
||||
// Import.
|
||||
expect(generated, contains("import 'package:foo/main.dart' as entrypoint;"));
|
||||
@ -89,6 +92,8 @@ void main() {
|
||||
// Main
|
||||
expect(generated, contains('ui.webOnlyWarmupEngine('));
|
||||
expect(generated, contains('entrypoint.main as _'));
|
||||
}, overrides: <Type, Generator>{
|
||||
TemplateRenderer: () => const MustacheTemplateRenderer(),
|
||||
}));
|
||||
|
||||
test('version.json is created after release build', () => testbed.run(() async {
|
||||
@ -196,6 +201,8 @@ void main() {
|
||||
|
||||
// Import.
|
||||
expect(generated, contains("import 'file:///other/lib/main.dart' as entrypoint;"));
|
||||
}, overrides: <Type, Generator>{
|
||||
TemplateRenderer: () => const MustacheTemplateRenderer(),
|
||||
}));
|
||||
|
||||
test('WebEntrypointTarget generates a plugin registrant for a file outside of main', () => testbed.run(() async {
|
||||
@ -210,7 +217,9 @@ void main() {
|
||||
|
||||
// Import.
|
||||
expect(generated, contains("import 'file:///other/lib/main.dart' as entrypoint;"));
|
||||
expect(generated, contains("import 'package:foo/generated_plugin_registrant.dart';"));
|
||||
expect(generated, contains("import 'web_plugin_registrant.dart' as pluginRegistrant;"));
|
||||
}, overrides: <Type, Generator>{
|
||||
TemplateRenderer: () => const MustacheTemplateRenderer(),
|
||||
}));
|
||||
|
||||
|
||||
@ -226,8 +235,8 @@ void main() {
|
||||
final String generated = environment.buildDir.childFile('main.dart').readAsStringSync();
|
||||
|
||||
// Plugins
|
||||
expect(generated, contains("import 'package:foo/generated_plugin_registrant.dart';"));
|
||||
expect(generated, contains('registerPlugins(webPluginRegistrar);'));
|
||||
expect(generated, contains("import 'web_plugin_registrant.dart' as pluginRegistrant;"));
|
||||
expect(generated, contains('pluginRegistrant.registerPlugins();'));
|
||||
|
||||
// Import.
|
||||
expect(generated, contains("import 'package:foo/main.dart' as entrypoint;"));
|
||||
@ -237,6 +246,7 @@ void main() {
|
||||
expect(generated, contains('entrypoint.main as _'));
|
||||
}, overrides: <Type, Generator>{
|
||||
Platform: () => windows,
|
||||
TemplateRenderer: () => const MustacheTemplateRenderer(),
|
||||
}));
|
||||
|
||||
test('WebEntrypointTarget generates an entrypoint without plugins and init platform', () => testbed.run(() async {
|
||||
@ -249,9 +259,9 @@ void main() {
|
||||
|
||||
final String generated = environment.buildDir.childFile('main.dart').readAsStringSync();
|
||||
|
||||
// Plugins
|
||||
expect(generated, isNot(contains("import 'package:foo/generated_plugin_registrant.dart';")));
|
||||
expect(generated, isNot(contains('registerPlugins(webPluginRegistrar);')));
|
||||
// Plugins (the generated file is a noop)
|
||||
expect(generated, contains("import 'web_plugin_registrant.dart' as pluginRegistrant;"));
|
||||
expect(generated, contains('pluginRegistrant.registerPlugins();'));
|
||||
|
||||
// Import.
|
||||
expect(generated, contains("import 'package:foo/main.dart' as entrypoint;"));
|
||||
@ -259,7 +269,8 @@ void main() {
|
||||
// Main
|
||||
expect(generated, contains('ui.webOnlyWarmupEngine('));
|
||||
expect(generated, contains('entrypoint.main as _'));
|
||||
|
||||
}, overrides: <Type, Generator>{
|
||||
TemplateRenderer: () => const MustacheTemplateRenderer(),
|
||||
}));
|
||||
|
||||
test('WebEntrypointTarget generates an entrypoint with a language version', () => testbed.run(() async {
|
||||
@ -273,6 +284,8 @@ void main() {
|
||||
|
||||
// Language version
|
||||
expect(generated, contains('// @dart=2.8'));
|
||||
}, overrides: <Type, Generator>{
|
||||
TemplateRenderer: () => const MustacheTemplateRenderer(),
|
||||
}));
|
||||
|
||||
test('WebEntrypointTarget generates an entrypoint with a language version from a package config', () => testbed.run(() async {
|
||||
@ -288,6 +301,8 @@ void main() {
|
||||
|
||||
// Language version
|
||||
expect(generated, contains('// @dart=2.7'));
|
||||
}, overrides: <Type, Generator>{
|
||||
TemplateRenderer: () => const MustacheTemplateRenderer(),
|
||||
}));
|
||||
|
||||
test('WebEntrypointTarget generates an entrypoint without plugins and without init platform', () => testbed.run(() async {
|
||||
@ -301,8 +316,8 @@ void main() {
|
||||
final String generated = environment.buildDir.childFile('main.dart').readAsStringSync();
|
||||
|
||||
// Plugins
|
||||
expect(generated, isNot(contains("import 'package:foo/generated_plugin_registrant.dart';")));
|
||||
expect(generated, isNot(contains('registerPlugins(webPluginRegistrar);')));
|
||||
expect(generated, contains("import 'web_plugin_registrant.dart' as pluginRegistrant;"));
|
||||
expect(generated, contains('pluginRegistrant.registerPlugins();'));
|
||||
|
||||
// Import.
|
||||
expect(generated, contains("import 'package:foo/main.dart' as entrypoint;"));
|
||||
@ -310,6 +325,8 @@ void main() {
|
||||
// Main
|
||||
expect(generated, contains('ui.webOnlyWarmupEngine('));
|
||||
expect(generated, contains('entrypoint.main as _'));
|
||||
}, overrides: <Type, Generator>{
|
||||
TemplateRenderer: () => const MustacheTemplateRenderer(),
|
||||
}));
|
||||
|
||||
test('Dart2JSTarget calls dart2js with expected args with csp', () => testbed.run(() async {
|
||||
@ -676,9 +693,9 @@ void main() {
|
||||
// Depends on resource file.
|
||||
expect(environment.buildDir.childFile('service_worker.d').readAsStringSync(),
|
||||
contains('a/a.txt'));
|
||||
// Contains NOTICES
|
||||
// Does NOT contain NOTICES
|
||||
expect(environment.outputDir.childFile('flutter_service_worker.js').readAsStringSync(),
|
||||
contains('NOTICES'));
|
||||
isNot(contains('NOTICES')));
|
||||
}));
|
||||
|
||||
test('WebServiceWorker contains baseUrl cache', () => testbed.run(() async {
|
||||
|
@ -1079,11 +1079,12 @@ dependencies:
|
||||
web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri}
|
||||
''');
|
||||
|
||||
await injectPlugins(flutterProject, webPlatform: true);
|
||||
final Directory destination = flutterProject.directory.childDirectory('lib');
|
||||
await injectBuildTimePluginFiles(flutterProject, webPlatform: true, destination: destination);
|
||||
|
||||
final File registrant = flutterProject.directory
|
||||
.childDirectory('lib')
|
||||
.childFile('generated_plugin_registrant.dart');
|
||||
.childFile('web_plugin_registrant.dart');
|
||||
|
||||
expect(registrant.existsSync(), isTrue);
|
||||
expect(registrant.readAsStringSync(), contains("import 'package:web_plugin_with_nested/src/web_plugin.dart';"));
|
||||
|
@ -0,0 +1,200 @@
|
||||
// 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.
|
||||
|
||||
// @dart = 2.8
|
||||
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/build_system/build_system.dart';
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
import 'package:flutter_tools/src/commands/build.dart';
|
||||
|
||||
import '../../../src/context.dart'; // legacy
|
||||
import '../../../src/test_build_system.dart';
|
||||
import '../../../src/test_flutter_command_runner.dart'; // legacy
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
Cache.flutterRoot = '';
|
||||
Cache.disableLocking();
|
||||
});
|
||||
|
||||
group('ScrubGeneratedPluginRegistrant', () {
|
||||
// The files this migration deals with
|
||||
File gitignore;
|
||||
File registrant;
|
||||
|
||||
// Environment overrides
|
||||
FileSystem fileSystem;
|
||||
ProcessManager processManager;
|
||||
BuildSystem buildSystem;
|
||||
|
||||
setUp(() {
|
||||
// Prepare environment overrides
|
||||
fileSystem = MemoryFileSystem.test();
|
||||
processManager = FakeProcessManager.any();
|
||||
buildSystem = TestBuildSystem.all(BuildResult(success: true));
|
||||
// Write some initial state into our testing filesystem
|
||||
setupFileSystemForEndToEndTest(fileSystem);
|
||||
// Initialize fileSystem references
|
||||
gitignore = fileSystem.file('.gitignore');
|
||||
registrant = fileSystem.file(fileSystem.path.join('lib', 'generated_plugin_registrant.dart'));
|
||||
});
|
||||
|
||||
testUsingContext('noop - nothing to do - build runs', () async {
|
||||
expect(gitignore.existsSync(), isFalse);
|
||||
expect(registrant.existsSync(), isFalse);
|
||||
|
||||
await createTestCommandRunner(BuildCommand())
|
||||
.run(<String>['build', 'web', '--no-pub']);
|
||||
|
||||
final Directory buildDir = fileSystem.directory(fileSystem.path.join('build', 'web'));
|
||||
expect(buildDir.existsSync(), true);
|
||||
}, overrides: <Type, Generator> {
|
||||
FileSystem: () => fileSystem,
|
||||
ProcessManager: () => processManager,
|
||||
BuildSystem: () => buildSystem,
|
||||
});
|
||||
|
||||
testUsingContext('noop - .gitignore does not reference generated_plugin_registrant.dart - untouched', () async {
|
||||
writeGitignore(fileSystem, mentionsPluginRegistrant: false);
|
||||
|
||||
final String contentsBeforeBuild = gitignore.readAsStringSync();
|
||||
expect(contentsBeforeBuild, isNot(contains('lib/generated_plugin_registrant.dart')));
|
||||
|
||||
await createTestCommandRunner(BuildCommand())
|
||||
.run(<String>['build', 'web', '--no-pub']);
|
||||
|
||||
expect(gitignore.readAsStringSync(), contentsBeforeBuild);
|
||||
}, overrides: <Type, Generator> {
|
||||
FileSystem: () => fileSystem,
|
||||
ProcessManager: () => processManager,
|
||||
BuildSystem: () => buildSystem,
|
||||
});
|
||||
|
||||
testUsingContext('.gitignore references generated_plugin_registrant - cleans it up', () async {
|
||||
writeGitignore(fileSystem);
|
||||
|
||||
expect(gitignore.existsSync(), isTrue);
|
||||
expect(gitignore.readAsStringSync(), contains('lib/generated_plugin_registrant.dart'));
|
||||
|
||||
await createTestCommandRunner(BuildCommand())
|
||||
.run(<String>['build', 'web', '--no-pub']);
|
||||
|
||||
expect(gitignore.readAsStringSync(), isNot(contains('lib/generated_plugin_registrant.dart')));
|
||||
}, overrides: <Type, Generator> {
|
||||
FileSystem: () => fileSystem,
|
||||
ProcessManager: () => processManager,
|
||||
BuildSystem: () => buildSystem,
|
||||
});
|
||||
|
||||
testUsingContext('generated_plugin_registrant.dart exists - gets deleted', () async {
|
||||
writeGeneratedPluginRegistrant(fileSystem);
|
||||
|
||||
expect(registrant.existsSync(), isTrue);
|
||||
|
||||
await createTestCommandRunner(BuildCommand())
|
||||
.run(<String>['build', 'web', '--no-pub']);
|
||||
|
||||
expect(registrant.existsSync(), isFalse);
|
||||
}, overrides: <Type, Generator> {
|
||||
FileSystem: () => fileSystem,
|
||||
ProcessManager: () => processManager,
|
||||
BuildSystem: () => buildSystem,
|
||||
});
|
||||
|
||||
testUsingContext('scrubs generated_plugin_registrant file and cleans .gitignore', () async {
|
||||
writeGitignore(fileSystem);
|
||||
writeGeneratedPluginRegistrant(fileSystem);
|
||||
|
||||
expect(registrant.existsSync(), isTrue);
|
||||
expect(gitignore.readAsStringSync(), contains('lib/generated_plugin_registrant.dart'));
|
||||
|
||||
await createTestCommandRunner(BuildCommand())
|
||||
.run(<String>['build', 'web', '--no-pub']);
|
||||
|
||||
expect(registrant.existsSync(), isFalse);
|
||||
expect(gitignore.readAsStringSync(), isNot(contains('lib/generated_plugin_registrant.dart')));
|
||||
}, overrides: <Type, Generator>{
|
||||
FileSystem: () => fileSystem,
|
||||
ProcessManager: () => processManager,
|
||||
BuildSystem: () => buildSystem,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Writes something that resembles the contents of Flutter's .gitignore file
|
||||
void writeGitignore(FileSystem fs, { bool mentionsPluginRegistrant = true }) {
|
||||
fs.file('.gitignore').createSync(recursive: true);
|
||||
fs.file('.gitignore')
|
||||
.writeAsStringSync('''
|
||||
/build/
|
||||
|
||||
# Web related
|
||||
${mentionsPluginRegistrant ? 'lib/generated_plugin_registrant.dart':'another_file.dart'}
|
||||
|
||||
# Symbolication related
|
||||
''');
|
||||
}
|
||||
|
||||
// Creates an empty generated_plugin_registrant.dart file
|
||||
void writeGeneratedPluginRegistrant(FileSystem fs) {
|
||||
final String path = fs.path.join('lib', 'generated_plugin_registrant.dart');
|
||||
fs.file(path).createSync(recursive: true);
|
||||
}
|
||||
|
||||
// Adds a bunch of files to the filesystem
|
||||
// (taken from commands.shard/hermetic/build_web_test.dart)
|
||||
void setupFileSystemForEndToEndTest(FileSystem fileSystem) {
|
||||
final List<String> dependencies = <String>[
|
||||
'.packages',
|
||||
fileSystem.path.join('web', 'index.html'),
|
||||
fileSystem.path.join('lib', 'main.dart'),
|
||||
fileSystem.path.join('packages', 'flutter_tools', 'lib', 'src', 'build_system', 'targets', 'web.dart'),
|
||||
fileSystem.path.join('bin', 'cache', 'flutter_web_sdk'),
|
||||
fileSystem.path.join('bin', 'cache', 'dart-sdk', 'bin', 'snapshots', 'dart2js.dart.snapshot'),
|
||||
fileSystem.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'),
|
||||
fileSystem.path.join('bin', 'cache', 'dart-sdk '),
|
||||
];
|
||||
for (final String dependency in dependencies) {
|
||||
fileSystem.file(dependency).createSync(recursive: true);
|
||||
}
|
||||
|
||||
// Project files.
|
||||
fileSystem.file('.packages')
|
||||
.writeAsStringSync('''
|
||||
foo:lib/
|
||||
fizz:bar/lib/
|
||||
''');
|
||||
fileSystem.file('pubspec.yaml')
|
||||
.writeAsStringSync('''
|
||||
name: foo
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
fizz:
|
||||
path:
|
||||
bar/
|
||||
''');
|
||||
fileSystem.file(fileSystem.path.join('bar', 'pubspec.yaml'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('''
|
||||
name: bar
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
platforms:
|
||||
web:
|
||||
pluginClass: UrlLauncherPlugin
|
||||
fileName: url_launcher_web.dart
|
||||
''');
|
||||
fileSystem.file(fileSystem.path.join('bar', 'lib', 'url_launcher_web.dart'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('''
|
||||
class UrlLauncherPlugin {}
|
||||
''');
|
||||
fileSystem.file(fileSystem.path.join('lib', 'main.dart'))
|
||||
.writeAsStringSync('void main() { }');
|
||||
}
|
@ -49,12 +49,20 @@ void main() {
|
||||
// the generated_plugin_registrant generation.
|
||||
await _addDependency(projectDir, 'shared_preferences',
|
||||
version: '^2.0.0');
|
||||
await _analyzeProject(projectDir);
|
||||
// The plugin registrant is only created after a build...
|
||||
await _buildWebProject(projectDir);
|
||||
|
||||
// Find the web_plugin_registrant, now that it lives outside "lib":
|
||||
final Directory buildDir = projectDir
|
||||
.childDirectory('.dart_tool/flutter_build')
|
||||
.listSync()
|
||||
.firstWhere((FileSystemEntity entity) => entity is Directory) as Directory;
|
||||
|
||||
expect(
|
||||
projectDir.childFile('lib/generated_plugin_registrant.dart'),
|
||||
buildDir.childFile('web_plugin_registrant.dart'),
|
||||
exists,
|
||||
);
|
||||
await _analyzeEntity(buildDir.childFile('web_plugin_registrant.dart'));
|
||||
}, overrides: <Type, Generator>{
|
||||
Pub: () => Pub(
|
||||
fileSystem: globals.fs,
|
||||
@ -88,12 +96,20 @@ void main() {
|
||||
'test_web_plugin_with_a_purposefully_extremely_long_package_name',
|
||||
path: '../test_plugin',
|
||||
);
|
||||
await _analyzeProject(projectDir);
|
||||
// The plugin registrant is only created after a build...
|
||||
await _buildWebProject(projectDir);
|
||||
|
||||
// Find the web_plugin_registrant, now that it lives outside "lib":
|
||||
final Directory buildDir = projectDir
|
||||
.childDirectory('.dart_tool/flutter_build')
|
||||
.listSync()
|
||||
.firstWhere((FileSystemEntity entity) => entity is Directory) as Directory;
|
||||
|
||||
expect(
|
||||
projectDir.childFile('lib/generated_plugin_registrant.dart'),
|
||||
buildDir.childFile('web_plugin_registrant.dart'),
|
||||
exists,
|
||||
);
|
||||
await _analyzeEntity(buildDir.childFile('web_plugin_registrant.dart'));
|
||||
}, overrides: <Type, Generator>{
|
||||
Pub: () => Pub(
|
||||
fileSystem: globals.fs,
|
||||
@ -215,7 +231,7 @@ ${linterRules.map((String rule) => ' - $rule').join('\n')}
|
||||
''');
|
||||
}
|
||||
|
||||
Future<void> _analyzeProject(Directory workingDir) async {
|
||||
Future<void> _analyzeEntity(FileSystemEntity target) async {
|
||||
final String flutterToolsSnapshotPath = globals.fs.path.absolute(
|
||||
globals.fs.path.join(
|
||||
'..',
|
||||
@ -229,6 +245,35 @@ Future<void> _analyzeProject(Directory workingDir) async {
|
||||
final List<String> args = <String>[
|
||||
flutterToolsSnapshotPath,
|
||||
'analyze',
|
||||
target.path,
|
||||
];
|
||||
|
||||
final ProcessResult exec = await Process.run(
|
||||
globals.artifacts.getHostArtifact(HostArtifact.engineDartBinary).path,
|
||||
args,
|
||||
workingDirectory: target is Directory ? target.path : target.dirname,
|
||||
);
|
||||
printOnFailure('Output of flutter analyze:');
|
||||
printOnFailure(exec.stdout.toString());
|
||||
printOnFailure(exec.stderr.toString());
|
||||
expect(exec.exitCode, 0);
|
||||
}
|
||||
|
||||
Future<void> _buildWebProject(Directory workingDir) async {
|
||||
final String flutterToolsSnapshotPath = globals.fs.path.absolute(
|
||||
globals.fs.path.join(
|
||||
'..',
|
||||
'..',
|
||||
'bin',
|
||||
'cache',
|
||||
'flutter_tools.snapshot',
|
||||
),
|
||||
);
|
||||
|
||||
final List<String> args = <String>[
|
||||
flutterToolsSnapshotPath,
|
||||
'build',
|
||||
'web',
|
||||
];
|
||||
|
||||
final ProcessResult exec = await Process.run(
|
||||
@ -236,7 +281,7 @@ Future<void> _analyzeProject(Directory workingDir) async {
|
||||
args,
|
||||
workingDirectory: workingDir.path,
|
||||
);
|
||||
printOnFailure('Output of flutter analyze:');
|
||||
printOnFailure('Output of flutter build web:');
|
||||
printOnFailure(exec.stdout.toString());
|
||||
printOnFailure(exec.stderr.toString());
|
||||
expect(exec.exitCode, 0);
|
Loading…
x
Reference in New Issue
Block a user