From b6e92003c8abc5e04a193b58e4a7c8b98d4b8536 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Fri, 22 Nov 2019 15:02:20 -0800 Subject: [PATCH] Add `.flutter-plugins-dependencies` to the project, which contains the app's plugin dependency graph (#45379) --- .gitignore | 1 + .../.gitignore | 1 + .../splash_screen_kitchen_sink/.gitignore | 1 + .../splash_screen_load_rotate/.gitignore | 1 + .../splash_screen_trans_rotate/.gitignore | 1 + .../ios_add2app/flutterapp/.gitignore | 1 + .../release_smoke_test/.gitignore | 1 + packages/flutter_tools/gradle/flutter.gradle | 77 +++++++++++-- packages/flutter_tools/lib/src/plugins.dart | 105 ++++++++++++++---- packages/flutter_tools/lib/src/project.dart | 4 + .../templates/app/.gitignore.tmpl | 1 + .../templates/module/common/.gitignore.tmpl | 1 + .../templates/package/.gitignore.tmpl | 1 + .../general.shard/plugin_parsing_test.dart | 8 +- .../test/general.shard/plugins_test.dart | 79 ++++++++++++- 15 files changed, 245 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index b48f326a80a..c407f64792f 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ version **/doc/api/ .dart_tool/ .flutter-plugins +.flutter-plugins-dependencies .packages .pub-cache/ .pub/ diff --git a/dev/integration_tests/android_embedding_v2_smoke_test/.gitignore b/dev/integration_tests/android_embedding_v2_smoke_test/.gitignore index 437cb45872e..ae1f1838ee7 100644 --- a/dev/integration_tests/android_embedding_v2_smoke_test/.gitignore +++ b/dev/integration_tests/android_embedding_v2_smoke_test/.gitignore @@ -24,6 +24,7 @@ **/doc/api/ .dart_tool/ .flutter-plugins +.flutter-plugins-dependencies .packages .pub-cache/ .pub/ diff --git a/dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/.gitignore b/dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/.gitignore index 05fc69bfc59..2b1f4958f2c 100644 --- a/dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/.gitignore +++ b/dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/.gitignore @@ -27,6 +27,7 @@ **/doc/api/ .dart_tool/ .flutter-plugins +.flutter-plugins-dependencies .packages .pub-cache/ .pub/ diff --git a/dev/integration_tests/android_splash_screens/splash_screen_load_rotate/.gitignore b/dev/integration_tests/android_splash_screens/splash_screen_load_rotate/.gitignore index 716b5a9621b..7391168ddae 100644 --- a/dev/integration_tests/android_splash_screens/splash_screen_load_rotate/.gitignore +++ b/dev/integration_tests/android_splash_screens/splash_screen_load_rotate/.gitignore @@ -24,6 +24,7 @@ **/doc/api/ .dart_tool/ .flutter-plugins +.flutter-plugins-dependencies .packages .pub-cache/ .pub/ diff --git a/dev/integration_tests/android_splash_screens/splash_screen_trans_rotate/.gitignore b/dev/integration_tests/android_splash_screens/splash_screen_trans_rotate/.gitignore index 716b5a9621b..7391168ddae 100644 --- a/dev/integration_tests/android_splash_screens/splash_screen_trans_rotate/.gitignore +++ b/dev/integration_tests/android_splash_screens/splash_screen_trans_rotate/.gitignore @@ -24,6 +24,7 @@ **/doc/api/ .dart_tool/ .flutter-plugins +.flutter-plugins-dependencies .packages .pub-cache/ .pub/ diff --git a/dev/integration_tests/ios_add2app/flutterapp/.gitignore b/dev/integration_tests/ios_add2app/flutterapp/.gitignore index cdecf14aaa3..86f469179f5 100644 --- a/dev/integration_tests/ios_add2app/flutterapp/.gitignore +++ b/dev/integration_tests/ios_add2app/flutterapp/.gitignore @@ -39,3 +39,4 @@ build/ .android/ .ios/ .flutter-plugins +.flutter-plugins-dependencies diff --git a/dev/integration_tests/release_smoke_test/.gitignore b/dev/integration_tests/release_smoke_test/.gitignore index 716b5a9621b..7391168ddae 100644 --- a/dev/integration_tests/release_smoke_test/.gitignore +++ b/dev/integration_tests/release_smoke_test/.gitignore @@ -24,6 +24,7 @@ **/doc/api/ .dart_tool/ .flutter-plugins +.flutter-plugins-dependencies .packages .pub-cache/ .pub/ diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle index 45dc4e36d54..173ba30c9a8 100644 --- a/packages/flutter_tools/gradle/flutter.gradle +++ b/packages/flutter_tools/gradle/flutter.gradle @@ -6,6 +6,7 @@ import static groovy.io.FileType.FILES import com.android.builder.model.AndroidProject import com.android.build.OutputFile +import groovy.json.JsonSlurper import java.nio.file.Path import java.nio.file.Paths import java.util.regex.Matcher @@ -253,6 +254,7 @@ class FlutterPlugin implements Plugin { private void configurePlugins() { if (!buildPluginAsAar()) { getPluginList().each this.&configurePluginProject + getPluginDependencies().each this.&configurePluginDependencies return } project.repositories { @@ -298,19 +300,15 @@ class FlutterPlugin implements Plugin { } // Adds the plugin project dependency to the app project . - private void configurePluginProject(String name, String _) { - Project pluginProject = project.rootProject.findProject(":$name") + private void configurePluginProject(String pluginName, String _) { + Project pluginProject = project.rootProject.findProject(":$pluginName") if (pluginProject == null) { - project.logger.error("Plugin project :$name not found. Please update settings.gradle.") + project.logger.error("Plugin project :$pluginName not found. Please update settings.gradle.") return } // Add plugin dependency to the app project. project.dependencies { - if (project.getConfigurations().findByName("implementation")) { - implementation pluginProject - } else { - compile pluginProject - } + implementation pluginProject } Closure addEmbeddingCompileOnlyDependency = { buildType -> String flutterBuildMode = buildModeFor(buildType) @@ -337,6 +335,36 @@ class FlutterPlugin implements Plugin { } } + // Add the dependencies on other plugin projects to the plugin project. + // A plugin A can depend on plugin B. As a result, this dependency must be surfaced by + // making the Gradle plugin project A depend on the Gradle plugin project B. + private void configurePluginDependencies(Object dependencyObject) { + assert dependencyObject.name instanceof String + Project pluginProject = project.rootProject.findProject(":${dependencyObject.name}") + if (pluginProject == null) { + // Ignore plugins that don't have a project since most likely they don't + // have an android/ directory. + return + } + assert dependencyObject.dependencies instanceof List + dependencyObject.dependencies.each { pluginDependencyName -> + assert pluginDependencyName instanceof String + if (pluginDependencyName.empty) { + return + } + Project dependencyProject = project.rootProject.findProject(":$pluginDependencyName") + if (dependencyProject == null) { + return + } + // Wait for the Android plugin to load and add the dependency to the plugin project. + pluginProject.afterEvaluate { + pluginProject.dependencies { + implementation dependencyProject + } + } + } + } + private Properties getPluginList() { File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins') Properties allPlugins = readPropertiesIfExist(pluginsFile) @@ -353,6 +381,39 @@ class FlutterPlugin implements Plugin { return androidPlugins } + // Gets the plugins dependencies from `.flutter-plugins-dependencies`. + private List getPluginDependencies() { + // Consider a `.flutter-plugins-dependencies` file with the following content: + // { + // "dependencyGraph": [ + // { + // "name": "plugin-a", + // "dependencies": ["plugin-b","plugin-c"] + // }, + // { + // "name": "plugin-b", + // "dependencies": ["plugin-c"] + // }, + // { + // "name": "plugin-c", + // "dependencies": []' + // } + // ] + // } + // + // This means, `plugin-a` depends on `plugin-b` and `plugin-c`. + // `plugin-b` depends on `plugin-c`. + // `plugin-c` doesn't depend on anything. + File pluginsDependencyFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins-dependencies') + if (pluginsDependencyFile.exists()) { + def object = new JsonSlurper().parseText(pluginsDependencyFile.text) + assert object instanceof Map + assert object.dependencyGraph instanceof List + return object.dependencyGraph + } + return [] + } + private static String toCammelCase(List parts) { if (parts.empty) { return "" diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index 2be328c7606..7e9b1246fcf 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart @@ -4,12 +4,14 @@ import 'dart:async'; +import 'package:meta/meta.dart'; import 'package:mustache/mustache.dart' as mustache; import 'package:yaml/yaml.dart'; import 'android/gradle.dart'; import 'base/common.dart'; import 'base/file_system.dart'; +import 'convert.dart'; import 'dart/package_map.dart'; import 'features.dart'; import 'globals.dart'; @@ -27,10 +29,14 @@ void _renderTemplateToFile(String template, dynamic context, String filePath) { class Plugin { Plugin({ - this.name, - this.path, - this.platforms, - }); + @required this.name, + @required this.path, + @required this.platforms, + @required this.dependencies, + }) : assert(name != null), + assert(path != null), + assert(platforms != null), + assert(dependencies != null); /// Parses [Plugin] specification from the provided pluginYaml. /// @@ -60,18 +66,28 @@ class Plugin { /// pluginClass: SamplePlugin /// windows: /// pluginClass: SamplePlugin - factory Plugin.fromYaml(String name, String path, YamlMap pluginYaml) { + factory Plugin.fromYaml( + String name, + String path, + YamlMap pluginYaml, + List dependencies, + ) { final List errors = validatePluginYaml(pluginYaml); if (errors.isNotEmpty) { throwToolExit('Invalid plugin specification.\n${errors.join('\n')}'); } if (pluginYaml != null && pluginYaml['platforms'] != null) { - return Plugin._fromMultiPlatformYaml(name, path, pluginYaml); + return Plugin._fromMultiPlatformYaml(name, path, pluginYaml, dependencies); } - return Plugin._fromLegacyYaml(name, path, pluginYaml); + return Plugin._fromLegacyYaml(name, path, pluginYaml, dependencies); } - factory Plugin._fromMultiPlatformYaml(String name, String path, dynamic pluginYaml) { + factory Plugin._fromMultiPlatformYaml( + String name, + String path, + dynamic pluginYaml, + List dependencies, + ) { assert (pluginYaml != null && pluginYaml['platforms'] != null, 'Invalid multi-platform plugin specification.'); final YamlMap platformsYaml = pluginYaml['platforms'] as YamlMap; @@ -118,10 +134,16 @@ class Plugin { name: name, path: path, platforms: platforms, + dependencies: dependencies, ); } - factory Plugin._fromLegacyYaml(String name, String path, dynamic pluginYaml) { + factory Plugin._fromLegacyYaml( + String name, + String path, + dynamic pluginYaml, + List dependencies, + ) { final Map platforms = {}; final String pluginClass = pluginYaml['pluginClass'] as String; if (pluginYaml != null && pluginClass != null) { @@ -147,6 +169,7 @@ class Plugin { name: name, path: path, platforms: platforms, + dependencies: dependencies, ); } @@ -232,6 +255,9 @@ class Plugin { final String name; final String path; + /// The name of the packages this plugin depends on. + final List dependencies; + /// This is a mapping from platform config key to the plugin platform spec. final Map platforms; } @@ -250,11 +276,13 @@ Plugin _pluginFromPubspec(String name, Uri packageRoot) { return null; } final String packageRootPath = fs.path.fromUri(packageRoot); + final YamlMap dependencies = pubspec['dependencies']; printTrace('Found plugin $name at $packageRootPath'); return Plugin.fromYaml( name, packageRootPath, flutterConfig['plugin'] as YamlMap, + dependencies == null ? [] : [...dependencies.keys], ); } @@ -281,29 +309,58 @@ List findPlugins(FlutterProject project) { return plugins; } -/// Returns true if .flutter-plugins has changed, otherwise returns false. +/// Writes the .flutter-plugins and .flutter-plugins-dependencies files based on the list of plugins. +/// If there aren't any plugins, then the files aren't written to disk. +/// +/// Finally, returns [true] if .flutter-plugins or .flutter-plugins-dependencies have changed, +/// otherwise returns [false]. bool _writeFlutterPluginsList(FlutterProject project, List plugins) { + final List directAppDependencies = []; + final StringBuffer flutterPluginsBuffer = StringBuffer(); + + final Set pluginNames = {}; + for (Plugin plugin in plugins) { + pluginNames.add(plugin.name); + } + for (Plugin plugin in plugins) { + flutterPluginsBuffer.write('${plugin.name}=${escapePath(plugin.path)}\n'); + directAppDependencies.add({ + 'name': plugin.name, + // Extract the plugin dependencies which happen to be plugins. + 'dependencies': [...plugin.dependencies.where(pluginNames.contains)], + }); + } final File pluginsFile = project.flutterPluginsFile; - final String oldContents = _readFlutterPluginsList(project); - final String pluginManifest = - plugins.map((Plugin p) => '${p.name}=${escapePath(p.path)}').join('\n'); - if (pluginManifest.isNotEmpty) { - pluginsFile.writeAsStringSync('$pluginManifest\n', flush: true); + final String oldPluginFileContent = _readFileContent(pluginsFile); + final String pluginFileContent = flutterPluginsBuffer.toString(); + if (pluginFileContent.isNotEmpty) { + pluginsFile.writeAsStringSync(pluginFileContent, flush: true); } else { if (pluginsFile.existsSync()) { pluginsFile.deleteSync(); } } - final String newContents = _readFlutterPluginsList(project); - return oldContents != newContents; + + final File dependenciesFile = project.flutterPluginsDependenciesFile; + final String oldDependenciesFileContent = _readFileContent(dependenciesFile); + final String dependenciesFileContent = json.encode({ + 'dependencyGraph': directAppDependencies, + }); + if (pluginFileContent.isNotEmpty) { + dependenciesFile.writeAsStringSync(dependenciesFileContent, flush: true); + } else { + if (dependenciesFile.existsSync()) { + dependenciesFile.deleteSync(); + } + } + + return oldPluginFileContent != _readFileContent(pluginsFile) + || oldDependenciesFileContent != _readFileContent(dependenciesFile); } -/// Returns the contents of the `.flutter-plugins` file in [project], or -/// null if that file does not exist. -String _readFlutterPluginsList(FlutterProject project) { - return project.flutterPluginsFile.existsSync() - ? project.flutterPluginsFile.readAsStringSync() - : null; +/// Returns the contents of [File] or [null] if that file does not exist. +String _readFileContent(File file) { + return file.existsSync() ? file.readAsStringSync() : null; } const String _androidPluginRegistryTemplateOldEmbedding = '''package io.flutter.plugins; @@ -782,5 +839,5 @@ Future injectPlugins(FlutterProject project, {bool checkProjects = false}) /// /// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`. bool hasPlugins(FlutterProject project) { - return _readFlutterPluginsList(project) != null; + return _readFileContent(project.flutterPluginsFile) != null; } diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 6751ab68f6a..2da69f14525 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -146,6 +146,10 @@ class FlutterProject { /// The `.flutter-plugins` file of this project. File get flutterPluginsFile => directory.childFile('.flutter-plugins'); + /// The `.flutter-plugins-dependencies` file of this project, + /// which contains the dependencies each plugin depends on. + File get flutterPluginsDependenciesFile => directory.childFile('.flutter-plugins-dependencies'); + /// The `.dart-tool` directory of this project. Directory get dartTool => directory.childDirectory('.dart_tool'); diff --git a/packages/flutter_tools/templates/app/.gitignore.tmpl b/packages/flutter_tools/templates/app/.gitignore.tmpl index 437cb45872e..ae1f1838ee7 100644 --- a/packages/flutter_tools/templates/app/.gitignore.tmpl +++ b/packages/flutter_tools/templates/app/.gitignore.tmpl @@ -24,6 +24,7 @@ **/doc/api/ .dart_tool/ .flutter-plugins +.flutter-plugins-dependencies .packages .pub-cache/ .pub/ diff --git a/packages/flutter_tools/templates/module/common/.gitignore.tmpl b/packages/flutter_tools/templates/module/common/.gitignore.tmpl index cdecf14aaa3..86f469179f5 100644 --- a/packages/flutter_tools/templates/module/common/.gitignore.tmpl +++ b/packages/flutter_tools/templates/module/common/.gitignore.tmpl @@ -39,3 +39,4 @@ build/ .android/ .ios/ .flutter-plugins +.flutter-plugins-dependencies diff --git a/packages/flutter_tools/templates/package/.gitignore.tmpl b/packages/flutter_tools/templates/package/.gitignore.tmpl index 6ffedaea4f1..bb431f0d5b4 100644 --- a/packages/flutter_tools/templates/package/.gitignore.tmpl +++ b/packages/flutter_tools/templates/package/.gitignore.tmpl @@ -24,6 +24,7 @@ **/doc/api/ .dart_tool/ .flutter-plugins +.flutter-plugins-dependencies .packages .pub-cache/ .pub/ diff --git a/packages/flutter_tools/test/general.shard/plugin_parsing_test.dart b/packages/flutter_tools/test/general.shard/plugin_parsing_test.dart index ea916797ff2..8f96ebb6492 100644 --- a/packages/flutter_tools/test/general.shard/plugin_parsing_test.dart +++ b/packages/flutter_tools/test/general.shard/plugin_parsing_test.dart @@ -20,7 +20,7 @@ void main() { final dynamic pluginYaml = loadYaml(pluginYamlRaw); final Plugin plugin = - Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml); + Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml, const []); final AndroidPlugin androidPlugin = plugin.platforms[AndroidPlugin.kConfigKey]; @@ -53,7 +53,7 @@ void main() { final dynamic pluginYaml = loadYaml(pluginYamlRaw); final Plugin plugin = - Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml); + Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml, const []); final AndroidPlugin androidPlugin = plugin.platforms[AndroidPlugin.kConfigKey]; @@ -100,7 +100,7 @@ void main() { final dynamic pluginYaml = loadYaml(pluginYamlRaw); final Plugin plugin = - Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml); + Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml, const []); final AndroidPlugin androidPlugin = plugin.platforms[AndroidPlugin.kConfigKey]; @@ -144,7 +144,7 @@ void main() { final dynamic pluginYaml = loadYaml(pluginYamlRaw); final Plugin plugin = - Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml); + Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml, const []); expect(plugin.platforms, {}); }); diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart index 802d80347b8..5d3540877c3 100644 --- a/packages/flutter_tools/test/general.shard/plugins_test.dart +++ b/packages/flutter_tools/test/general.shard/plugins_test.dart @@ -9,7 +9,7 @@ import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/plugins.dart'; import 'package:flutter_tools/src/project.dart'; - +import 'package:meta/meta.dart'; import 'package:mockito/mockito.dart'; import '../src/common.dart'; @@ -32,7 +32,8 @@ void main() { // Add basic properties to the Flutter project and subprojects flutterProject = MockFlutterProject(); when(flutterProject.directory).thenReturn(fs.directory('/')); - when(flutterProject.flutterPluginsFile).thenReturn(flutterProject.directory.childFile('.plugins')); + when(flutterProject.flutterPluginsFile).thenReturn(flutterProject.directory.childFile('.flutter-plugins')); + when(flutterProject.flutterPluginsDependenciesFile).thenReturn(flutterProject.directory.childFile('.flutter-plugins-dependencies')); iosProject = MockIosProject(); when(flutterProject.ios).thenReturn(iosProject); when(iosProject.pluginRegistrantHost).thenReturn(flutterProject.directory.childDirectory('Runner')); @@ -189,6 +190,37 @@ flutter: ); } + void createPluginWithDependencies({ + @required String name, + @required List dependencies, + }) { + assert(name != null); + assert(dependencies != null); + + final Directory pluginDirectory = fs.systemTempDirectory.createTempSync('plugin.'); + pluginDirectory + .childFile('pubspec.yaml') + .writeAsStringSync(''' +name: $name +flutter: + plugin: + androidPackage: plugin2 + pluginClass: UseNewEmbedding +dependencies: +'''); + for (String dependency in dependencies) { + pluginDirectory + .childFile('pubspec.yaml') + .writeAsStringSync(' $dependency:\n', mode: FileMode.append); + } + flutterProject.directory + .childFile('.packages') + .writeAsStringSync( + '$name:${pluginDirectory.childDirectory('lib').uri.toString()}\n', + mode: FileMode.append, + ); + } + // Creates the files that would indicate that pod install has run for the // given project. void simulatePodInstallRun(XcodeBasedProject project) { @@ -199,6 +231,7 @@ flutter: testUsingContext('Refreshing the plugin list is a no-op when the plugins list stays empty', () { refreshPluginsList(flutterProject); expect(flutterProject.flutterPluginsFile.existsSync(), false); + expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), false); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), @@ -210,6 +243,7 @@ flutter: when(macosProject.existsSync()).thenReturn(false); refreshPluginsList(flutterProject); expect(flutterProject.flutterPluginsFile.existsSync(), false); + expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), false); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), @@ -221,6 +255,47 @@ flutter: when(macosProject.existsSync()).thenReturn(false); refreshPluginsList(flutterProject); expect(flutterProject.flutterPluginsFile.existsSync(), true); + expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Refreshing the plugin list modifies .flutter-plugins and .flutter-plugins-dependencies when there are plugins', () { + createPluginWithDependencies(name: 'plugin-a', dependencies: const ['plugin-b', 'plugin-c', 'random-package']); + createPluginWithDependencies(name: 'plugin-b', dependencies: const ['plugin-c']); + createPluginWithDependencies(name: 'plugin-c', dependencies: const []); + when(iosProject.existsSync()).thenReturn(false); + when(macosProject.existsSync()).thenReturn(false); + + refreshPluginsList(flutterProject); + + expect(flutterProject.flutterPluginsFile.existsSync(), true); + expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true); + expect(flutterProject.flutterPluginsFile.readAsStringSync(), + 'plugin-a=/.tmp_rand0/plugin.rand0/\n' + 'plugin-b=/.tmp_rand0/plugin.rand1/\n' + 'plugin-c=/.tmp_rand0/plugin.rand2/\n' + '' + ); + expect(flutterProject.flutterPluginsDependenciesFile.readAsStringSync(), + '{' + '"dependencyGraph":[' + '{' + '"name":"plugin-a",' + '"dependencies":["plugin-b","plugin-c"]' + '},' + '{' + '"name":"plugin-b",' + '"dependencies":["plugin-c"]' + '},' + '{' + '"name":"plugin-c",' + '"dependencies":[]' + '}' + ']' + '}' + ); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(),