|
| 1 | +import 'dart:convert'; |
| 2 | +import 'dart:io'; |
| 3 | + |
| 4 | +import 'package:freezed_annotation/freezed_annotation.dart'; |
| 5 | +import 'package:stacked_generator/src/generators/exceptions/config_file_not_found_exception.dart'; |
| 6 | +import 'package:stacked_generator/src/helpers/file_helper.dart'; |
| 7 | +import 'package:stacked_generator/src/models/stacked_config.dart'; |
| 8 | + |
| 9 | +const kConfigFileMalformed = |
| 10 | + 'Your configuration file is malformed. Double check to make sure you have properly formatted json.'; |
| 11 | +const kConfigFileName = 'stacked.json'; |
| 12 | +const kConfigFileNotFound = |
| 13 | + 'No configuration file found. Default Stacked values will be used.'; |
| 14 | +const kConfigFileNotFoundRetry = |
| 15 | + 'No configuration file found. Please, verify the config path passed as argument.'; |
| 16 | +const kDeprecatedPaths = |
| 17 | + 'Paths on Stacked config do not need to start with directory "lib" or "test" because are mandatory directories, defined by the Flutter framework. Stacked cli will not accept paths starting with "lib" or "test" after the next minor release.'; |
| 18 | + |
| 19 | +/// Handles stacked app configuration |
| 20 | +class ConfigHelper { |
| 21 | + final FileHelper fileHelper; |
| 22 | + ConfigHelper({required this.fileHelper}); |
| 23 | + |
| 24 | + /// Default config map used to compare and replace with custom values. |
| 25 | + final Map<String, dynamic> _defaultConfig = StackedConfig().toJson(); |
| 26 | + |
| 27 | + /// Custom config used to store custom values read from file. |
| 28 | + StackedConfig _customConfig = StackedConfig(); |
| 29 | + |
| 30 | + bool _hasCustomConfig = false; |
| 31 | + |
| 32 | + bool get hasCustomConfig => _hasCustomConfig; |
| 33 | + |
| 34 | + /// Relative services path for import statements. |
| 35 | + String get serviceImportPath => _customConfig.servicesPath; |
| 36 | + |
| 37 | + /// Relative path where services will be genereated. |
| 38 | + String get servicePath => _customConfig.servicesPath; |
| 39 | + |
| 40 | + /// Returns the name of the locator to use when registering service mocks. |
| 41 | + String get locatorName => _customConfig.locatorName; |
| 42 | + |
| 43 | + String get registerMocksFunction => _customConfig.registerMocksFunction; |
| 44 | + |
| 45 | + /// Relative import path related to services of test helpers and mock services. |
| 46 | + String get serviceTestHelpersImport => getFilePathToHelpersAndMocks( |
| 47 | + _customConfig.testServicesPath, |
| 48 | + ); |
| 49 | + |
| 50 | + /// Relative bottom sheet path for import statements. |
| 51 | + String get bottomSheetsPath => _customConfig.bottomSheetsPath; |
| 52 | + |
| 53 | + /// File path where bottom sheet builders are located. |
| 54 | + String get bottomSheetBuilderFilePath => |
| 55 | + _customConfig.bottomSheetBuilderFilePath; |
| 56 | + |
| 57 | + /// File path where bottom sheet type enum values are located. |
| 58 | + String get bottomSheetTypeFilePath => _customConfig.bottomSheetTypeFilePath; |
| 59 | + |
| 60 | + /// Relative path where dialogs will be genereated. |
| 61 | + String get dialogsPath => _customConfig.dialogsPath; |
| 62 | + |
| 63 | + /// File path where dialog builders are located. |
| 64 | + String get dialogBuilderFilePath => _customConfig.dialogBuilderFilePath; |
| 65 | + |
| 66 | + /// File path where dialog type enum values are located. |
| 67 | + String get dialogTypeFilePath => _customConfig.dialogTypeFilePath; |
| 68 | + |
| 69 | + /// File path where StackedApp is setup. |
| 70 | + String get stackedAppFilePath => _customConfig.stackedAppFilePath; |
| 71 | + |
| 72 | + /// File path where register functions for unit test setup and mock |
| 73 | + /// declarations are located. |
| 74 | + String get testHelpersFilePath => _customConfig.testHelpersFilePath; |
| 75 | + |
| 76 | + /// Relative path where services unit tests will be genereated. |
| 77 | + String get testServicesPath => _customConfig.testServicesPath; |
| 78 | + |
| 79 | + /// Relative path where viewmodels unit tests will be genereated. |
| 80 | + String get testViewsPath => _customConfig.testViewsPath; |
| 81 | + |
| 82 | + /// Relative views path for import statements. |
| 83 | + String get viewImportPath => _customConfig.viewsPath; |
| 84 | + |
| 85 | + /// Relative path where views and viewmodels will be genereated. |
| 86 | + String get viewPath => _customConfig.viewsPath; |
| 87 | + |
| 88 | + /// Relative import path related to viewmodels of test helpers and mock services. |
| 89 | + String get viewTestHelpersImport => getFilePathToHelpersAndMocks( |
| 90 | + _customConfig.testViewsPath, |
| 91 | + ); |
| 92 | + |
| 93 | + /// Relative path where widgets will be genereated. |
| 94 | + String get widgetPath => _customConfig.widgetsPath; |
| 95 | + |
| 96 | + /// Relative import path related to widget models of test helpers and mock services. |
| 97 | + String get widgetTestHelpersImport => getFilePathToHelpersAndMocks( |
| 98 | + _customConfig.testWidgetsPath, |
| 99 | + ); |
| 100 | + |
| 101 | + /// Returns boolean value to determine view builder style. |
| 102 | + /// |
| 103 | + /// False: StackedView |
| 104 | + /// True: ViewModelBuilder |
| 105 | + bool get v1 => _customConfig.v1; |
| 106 | + |
| 107 | + /// Returns int value for line length when format code. |
| 108 | + int get lineLength => _customConfig.lineLength; |
| 109 | + |
| 110 | + /// Returns boolean to indicate if the project prefers web templates |
| 111 | + bool get preferWeb => _customConfig.preferWeb; |
| 112 | + |
| 113 | + /// Composes configuration file and loads it into memory. |
| 114 | + /// |
| 115 | + /// Generally used to load the configuration file at root of the project. |
| 116 | + Future<void> composeAndLoadConfigFile({ |
| 117 | + String? configFilePath, |
| 118 | + String? projectPath, |
| 119 | + }) async { |
| 120 | + try { |
| 121 | + final configPath = await composeConfigFile( |
| 122 | + configFilePath: configFilePath, |
| 123 | + projectPath: projectPath, |
| 124 | + ); |
| 125 | + |
| 126 | + await loadConfig(configPath); |
| 127 | + } on ConfigFileNotFoundException catch (e) { |
| 128 | + if (e.shouldHaltCommand) rethrow; |
| 129 | + |
| 130 | + stdout.writeln(e.message); |
| 131 | + } catch (e) { |
| 132 | + stdout.writeln(e.toString()); |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + /// Returns configuration file path. |
| 137 | + /// |
| 138 | + /// When configFilePath is NOT null should returns configFilePath unless the |
| 139 | + /// file does NOT exists where should throw a ConfigFileNotFoundException. |
| 140 | + /// |
| 141 | + /// When configFilePath is null should returns [kConfigFileName] or with the |
| 142 | + /// []projectPath] included if it was passed through arguments. |
| 143 | + @visibleForTesting |
| 144 | + Future<String> composeConfigFile({ |
| 145 | + String? configFilePath, |
| 146 | + String? projectPath, |
| 147 | + }) async { |
| 148 | + if (configFilePath != null) { |
| 149 | + if (await fileHelper.fileExists(filePath: configFilePath)) { |
| 150 | + return configFilePath; |
| 151 | + } |
| 152 | + |
| 153 | + throw ConfigFileNotFoundException( |
| 154 | + kConfigFileNotFoundRetry, |
| 155 | + shouldHaltCommand: true, |
| 156 | + ); |
| 157 | + } |
| 158 | + |
| 159 | + if (projectPath != null) { |
| 160 | + return '$projectPath/$kConfigFileName'; |
| 161 | + } |
| 162 | + |
| 163 | + return kConfigFileName; |
| 164 | + } |
| 165 | + |
| 166 | + /// Reads configuration file and sets data to [_customConfig] map. |
| 167 | + @visibleForTesting |
| 168 | + Future<void> loadConfig(String configFilePath) async { |
| 169 | + try { |
| 170 | + final data = await fileHelper.readFileAsString( |
| 171 | + filePath: configFilePath, |
| 172 | + ); |
| 173 | + _customConfig = StackedConfig.fromJson(jsonDecode(data)); |
| 174 | + _hasCustomConfig = true; |
| 175 | + _sanitizeCustomConfig(); |
| 176 | + } on ConfigFileNotFoundException catch (e) { |
| 177 | + if (e.shouldHaltCommand) rethrow; |
| 178 | + |
| 179 | + stdout.writeln(e.message); |
| 180 | + } on FormatException catch (_) { |
| 181 | + stdout.writeln(kConfigFileMalformed); |
| 182 | + } catch (e) { |
| 183 | + stdout.writeln(e.toString()); |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + /// Replaces the default configuration in [path] by custom configuration |
| 188 | + /// available at [customConfig]. |
| 189 | + /// |
| 190 | + /// If [hasCustomConfig] is false, returns [path] without modifications. |
| 191 | + String replaceCustomPaths(String path) { |
| 192 | + if (!hasCustomConfig) return path; |
| 193 | + |
| 194 | + final customConfig = _customConfig.toJson(); |
| 195 | + String customPath = path; |
| 196 | + |
| 197 | + for (var k in _defaultConfig.keys) { |
| 198 | + // Avoid trying to replace non path values like v1 or lineLength |
| 199 | + if (!k.contains('path')) continue; |
| 200 | + |
| 201 | + if (customPath.contains(_defaultConfig[k])) { |
| 202 | + customPath = customPath.replaceFirst( |
| 203 | + _defaultConfig[k], |
| 204 | + customConfig[k], |
| 205 | + ); |
| 206 | + break; |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + return customPath; |
| 211 | + } |
| 212 | + |
| 213 | + /// Sanitizes the [path] removing [find]. |
| 214 | + /// |
| 215 | + /// Generally used to remove unnecessary parts of the path as {lib} or {test}. |
| 216 | + @visibleForTesting |
| 217 | + String sanitizePath(String path, [String find = 'lib/']) { |
| 218 | + if (!path.startsWith(find)) return path; |
| 219 | + |
| 220 | + return path.replaceFirst(find, ''); |
| 221 | + } |
| 222 | + |
| 223 | + /// Sanitizes [_customConfig] to remove unnecessary {lib} or {test} from paths. |
| 224 | + /// |
| 225 | + /// Warns the user if the custom config has deprecated path parts. |
| 226 | + void _sanitizeCustomConfig() { |
| 227 | + final sanitizedConfig = _customConfig.copyWith( |
| 228 | + stackedAppFilePath: sanitizePath(_customConfig.stackedAppFilePath), |
| 229 | + servicesPath: sanitizePath(_customConfig.servicesPath), |
| 230 | + viewsPath: sanitizePath(_customConfig.viewsPath), |
| 231 | + testHelpersFilePath: |
| 232 | + sanitizePath(_customConfig.testHelpersFilePath, 'test/'), |
| 233 | + testServicesPath: sanitizePath(_customConfig.testServicesPath, 'test/'), |
| 234 | + testViewsPath: sanitizePath(_customConfig.testViewsPath, 'test/'), |
| 235 | + ); |
| 236 | + |
| 237 | + if (_customConfig == sanitizedConfig) return; |
| 238 | + |
| 239 | + stdout.writeln(kDeprecatedPaths); |
| 240 | + |
| 241 | + _customConfig = sanitizedConfig; |
| 242 | + } |
| 243 | + |
| 244 | + /// Returns file path of test helpers and mock services relative to [path]. |
| 245 | + @visibleForTesting |
| 246 | + String getFilePathToHelpersAndMocks(String path) { |
| 247 | + String fileToImport = testHelpersFilePath; |
| 248 | + final pathSegments = |
| 249 | + path.split('/').where((element) => !element.contains('.')); |
| 250 | + |
| 251 | + for (var i = 0; i < pathSegments.length; i++) { |
| 252 | + fileToImport = '../$fileToImport'; |
| 253 | + } |
| 254 | + |
| 255 | + return fileToImport; |
| 256 | + } |
| 257 | + |
| 258 | + /// Exports custom config as a formatted Json String. |
| 259 | + String exportConfig() { |
| 260 | + return const JsonEncoder.withIndent(" ").convert(_customConfig.toJson()); |
| 261 | + } |
| 262 | + |
| 263 | + /// Overrides [widgets_path] value on configuration. |
| 264 | + void setWidgetsPath(String? path) { |
| 265 | + _customConfig = _customConfig.copyWith( |
| 266 | + widgetsPath: path ?? _customConfig.widgetsPath, |
| 267 | + ); |
| 268 | + } |
| 269 | +} |
0 commit comments