Skip to content

Commit e9afa20

Browse files
authored
feat: read stacked config (#24)
* feat: read stacked config - Added `xdg_directories` package - Updated packages - Added StackedConfig model - Added ConfigFileNotFoundException - Added helper classes - Updated BaseGenerator to return FutureOr String - Updated generators - Updated bottomsheet and dialog generators to take StackedAppFileName into account - Added ConfigHelper unit tests
1 parent 1732eeb commit e9afa20

22 files changed

+2604
-42
lines changed

lib/src/generators/base_generator.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import 'dart:async';
2+
13
/// Provides functionality common to all Generators
24
abstract class BaseGenerator {
3-
String generate();
5+
FutureOr<String> generate();
46
}
57

68
mixin StringBufferUtils {

lib/src/generators/bottomsheets/generate/bottomsheet_class_generator.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:stacked_generator/src/generators/base_generator.dart';
22
import 'package:stacked_generator/src/generators/bottomsheets/bottomsheet_config.dart';
33
import 'package:stacked_generator/src/generators/bottomsheets/generate/bottomsheet_class_generator_helper.dart';
4+
import 'package:stacked_generator/utils.dart';
45

56
import 'bottomsheet_class_content.dart';
67

@@ -12,8 +13,10 @@ class BottomsheetClassGenerator extends BottomsheetClassGeneratorHelper
1213
BottomsheetClassGenerator(this.bottomsheetConfigs, {this.locatorName});
1314

1415
@override
15-
String generate() {
16-
writeStackedservicesAndGeneratedLocaterImports();
16+
Future<String> generate() async {
17+
writeStackedservicesAndGeneratedLocaterImports(
18+
await getStackedAppFileName(),
19+
);
1720

1821
writeBottomsheetsImports(bottomsheetConfigs);
1922

lib/src/generators/bottomsheets/generate/bottomsheet_class_generator_helper.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ class BottomsheetClassGeneratorHelper with StringBufferUtils {
1616
write(setupBottomsheetHeader(locatorName));
1717
}
1818

19-
void writeStackedservicesAndGeneratedLocaterImports() {
19+
void writeStackedservicesAndGeneratedLocaterImports(String name) {
2020
writeLine();
2121
writeLine("import 'package:stacked_services/stacked_services.dart';");
2222
writeLine();
23-
writeLine("import 'app.locator.dart';");
23+
writeLine("import '$name.locator.dart';");
2424
}
2525

2626
void writeBottomsheetsImports(List<BottomsheetConfig> bottomsheetConfigs) {

lib/src/generators/dialogs/generate/dialog_class_generator.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:stacked_generator/src/generators/base_generator.dart';
22
import 'package:stacked_generator/src/generators/dialogs/dialog_config.dart';
33
import 'package:stacked_generator/src/generators/dialogs/generate/dialog_class_generator_helper.dart';
4+
import 'package:stacked_generator/utils.dart';
45

56
import 'dialog_class_content.dart';
67

@@ -12,8 +13,10 @@ class DialogClassGenerator extends DialogClassGeneratorHelper
1213
DialogClassGenerator(this.dialogConfigs, {this.locatorName});
1314

1415
@override
15-
String generate() {
16-
writeStackedservicesAndGeneratedLocaterImports();
16+
Future<String> generate() async {
17+
writeStackedservicesAndGeneratedLocaterImports(
18+
await getStackedAppFileName(),
19+
);
1720

1821
writeDialogsImports(dialogConfigs);
1922

lib/src/generators/dialogs/generate/dialog_class_generator_helper.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ class DialogClassGeneratorHelper with StringBufferUtils {
1616
write(setupDialogHeader(locatorName));
1717
}
1818

19-
void writeStackedservicesAndGeneratedLocaterImports() {
19+
void writeStackedservicesAndGeneratedLocaterImports(String name) {
2020
writeLine();
2121
writeLine("import 'package:stacked_services/stacked_services.dart';");
2222
writeLine();
23-
writeLine("import 'app.locator.dart';");
23+
writeLine("import '$name.locator.dart';");
2424
}
2525

2626
void writeDialogsImports(List<DialogConfig> dialogConfigs) {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class ConfigFileNotFoundException implements Exception {
2+
final String message;
3+
final bool shouldHaltCommand;
4+
ConfigFileNotFoundException(this.message, {this.shouldHaltCommand = false});
5+
6+
@override
7+
String toString() {
8+
return message;
9+
}
10+
}

lib/src/generators/router/generator/navigate_extension_class/navigate_extension_class_builder_helper.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import 'package:stacked_generator/src/generators/router_common/models/route_conf
55
import 'package:stacked_generator/src/generators/router_common/models/route_parameter_config.dart';
66
import 'package:stacked_generator/utils.dart';
77

8-
class NavigateExtensionClassBuilderHelper {
8+
mixin NavigateExtensionClassBuilderHelper {
99
Iterable<Method> buildNavigateToExtensionMethods(
1010
List<RouteConfig> routes,
1111
DartEmitter emitter,

lib/src/generators/router/generator/router_class/router_class_builder_helper.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import 'package:stacked_generator/src/generators/router_common/models/route_conf
33
import 'package:stacked_generator/src/generators/router_common/models/route_parameter_config.dart';
44
import 'package:stacked_generator/utils.dart';
55

6-
class RouterClassBuilderHelper {
6+
mixin RouterClassBuilderHelper {
77
/// Example
88
///
99
/// final _routes = <_i1.RouteDef>[

lib/src/generators/router_2/router_extension_builder/router_extension_builder_helper.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import 'package:stacked_generator/src/generators/router_2/code_builder/route_inf
44
import 'package:stacked_generator/src/generators/router_common/models/route_config.dart';
55
import 'package:stacked_generator/src/generators/router_common/models/route_parameter_config.dart';
66

7-
class RouterExtensionBuilderHelper {
7+
mixin RouterExtensionBuilderHelper {
88
Iterable<Method> buildNavigateToExtensionMethods(
99
List<RouteConfig> routes,
1010
DartEmitter emitter,

lib/src/helpers/config_helper.dart

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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

Comments
 (0)