Unverified Commit 1af8cc11 authored by David Iglesias's avatar David Iglesias Committed by GitHub

[tools][web] Make Plugin Registrant file ephemeral. (#102185)

parent 40627e9e
......@@ -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,
......
......@@ -550,23 +550,32 @@ Depends on all your plugins, and provides a function to register them.
end
''';
const String _noopDartPluginRegistryTemplate = '''
// Flutter web plugin registrant file.
//
// Generated file. Do not edit.
//
// ignore_for_file: type=lint
void registerPlugins() {}
''';
const String _dartPluginRegistryTemplate = '''
// 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
{{#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 {
......
// 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;
});
});
})
);
}
''';
}
// 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');
}
// 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';"));
......
// 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,15 +245,44 @@ 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: workingDir.path,
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(
globals.artifacts.getHostArtifact(HostArtifact.engineDartBinary).path,
args,
workingDirectory: workingDir.path,
);
printOnFailure('Output of flutter build web:');
printOnFailure(exec.stdout.toString());
printOnFailure(exec.stderr.toString());
expect(exec.exitCode, 0);
}
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