Skip to content

Commit 654ce9a

Browse files
committed
v6.25.0
1 parent b3e78d6 commit 654ce9a

File tree

8 files changed

+223
-12
lines changed

8 files changed

+223
-12
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## [6.25.0] - 2025-04-08
2+
3+
* Added: `makeCommand` to MetroService class. This will allow you to create a custom command in your project.
4+
* Added: `addPackages` method to MetroService
5+
* Added: `dev` parameter to `addPackages` and `addPackage` methods
6+
* Added: `withQueryParams` to RouteView class
7+
* Added: `hasQueryParameter(String key)` to NyState class. This will allow you to check if a query parameter exists in the current route.
8+
* Update: `runProcess` to connect all streams to the process
9+
110
## [6.24.0] - 2025-04-02
211

312
* Merge PR contribution from [voytech-net](https://github.com/voytech-net) #37

lib/helpers/extensions.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2413,6 +2413,21 @@ extension RouteViewExt on RouteView {
24132413
return path;
24142414
}
24152415

2416+
/// Add query parameters to the path.
2417+
String withQueryParams(Map<String, dynamic> args) {
2418+
String path = this.$1;
2419+
// build the query string
2420+
String queryString =
2421+
args.entries.map((entry) => "${entry.key}=${entry.value}").join("&");
2422+
// add the query string to the path
2423+
if (path.contains("?")) {
2424+
path = path.replaceAll("?", "?$queryString&");
2425+
} else {
2426+
path = "$path?$queryString";
2427+
}
2428+
return path;
2429+
}
2430+
24162431
/// Get the state name of the route.
24172432
String stateName() {
24182433
String fullPath = this.$2.toString();

lib/metro/constants/strings.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const String bottomNavFlag = 'bottom-nav';
1313

1414
// options
1515
const String postmanCollectionOption = 'postman';
16+
const String commandCategoryOption = 'category';
1617

1718
// folders
1819
const String yamlPath = 'pubspec.yaml';
@@ -29,6 +30,7 @@ const String networkingInterceptorsFolder =
2930
'lib/app/networking/dio/interceptors';
3031
const String bootstrapFolder = 'lib/bootstrap';
3132
const String configFolder = 'lib/config';
33+
const String commandsFolder = 'lib/app/commands';
3234
const String themeColorsFolder = 'lib/resources/themes/styles';
3335
const String routeGuardsFolder = 'lib/routes/guards';
3436
const String publicAssetsImagesFolder = 'public/images';
@@ -49,3 +51,5 @@ String makeImportPathBootstrap(String name) =>
4951
"import '/bootstrap/$name.dart';";
5052
String makeImportPathInterceptor(String name) =>
5153
"import '/app/networking/dio/interceptors/$name.dart';";
54+
String makeImportPathCommand(String name) =>
55+
"import '/app/commands/$name.dart';";

lib/metro/metro_service.dart

Lines changed: 170 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:convert';
12
import 'dart:io';
23
import '/metro/constants/strings.dart';
34
import '/metro/metro_console.dart';
@@ -37,7 +38,6 @@ class MetroService {
3738
}
3839

3940
argumentsForAction.removeAt(0);
40-
4141
await nyCommand.action!(argumentsForAction);
4242
}
4343

@@ -327,19 +327,49 @@ import '/resources/themes/${nameReCase.snakeCase}_theme.dart';""";
327327
}
328328

329329
/// Runs a process
330-
static runProcess(String command) async {
330+
static Future<int> runProcess(String command) async {
331331
List<String> commands = command.split(" ");
332332

333333
final processArguments = commands.getRange(1, commands.length).toList();
334-
final process = await Process.start(commands.first, processArguments,
335-
runInShell: false);
336-
await stdout.addStream(process.stdout);
337-
await stderr.addStream(process.stderr);
334+
335+
final process =
336+
await Process.start(commands.first, processArguments, runInShell: true);
337+
338+
// Connect all streams
339+
process.stdout.pipe(stdout);
340+
process.stderr.pipe(stderr);
341+
stdin.pipe(process.stdin); // This pipes stdin to the child process
342+
343+
final exitCode = await process.exitCode;
344+
345+
if (exitCode != 0) {
346+
MetroConsole.writeInRed("Error: $exitCode");
347+
}
348+
349+
return exitCode;
338350
}
339351

340352
/// Add a package to your pubspec.yaml file.
341-
static addPackage(String package) async {
342-
await runProcess("dart pub add $package");
353+
static addPackage(String package, {String? version, bool dev = false}) async {
354+
String command = "dart pub add";
355+
if (dev) {
356+
command += " --dev";
357+
}
358+
command += " $package";
359+
if (version != null) {
360+
command += ":$version";
361+
}
362+
await runProcess(command);
363+
}
364+
365+
/// Add a packages to your pubspec.yaml file.
366+
static addPackages(List<String> packages, {bool dev = false}) async {
367+
String command = "dart pub add";
368+
if (dev) {
369+
command += " --dev";
370+
}
371+
command += " ${packages.join(" ")}";
372+
await runProcess(command);
343373
}
344374

345375
/// Creates a new Model.
@@ -444,6 +474,63 @@ final Map<Type, dynamic> modelDecoders = {${reg.allMatches(file).map((e) => e.gr
444474
});
445475
}
446476

477+
/// Creates a new command file.
478+
static makeCommand(String commandName, String value,
479+
{String folderPath = commandsFolder,
480+
bool forceCreate = false,
481+
String? creationPath,
482+
String? category}) async {
483+
String name = commandName.replaceAll(RegExp(r'(_?command)'), "");
484+
ReCase nameReCase = ReCase(name);
485+
486+
// create missing directories in the project
487+
await _makeDirectory(folderPath);
488+
await createDirectoriesFromCreationPath(creationPath, folderPath);
489+
490+
// Check custom_commands.json file exists
491+
String customCommandsFilePath = "$folderPath/custom_commands.json";
492+
if (!await hasFile(customCommandsFilePath)) {
493+
await _createNewFile(customCommandsFilePath, "[]");
494+
}
495+
496+
// create file path
497+
String filePath = createPathForDartFile(
498+
folderPath: folderPath, className: name, creationPath: creationPath);
499+
await _checkIfFileExists(filePath, shouldForceCreate: forceCreate);
500+
await _createNewFile(filePath, value, onSuccess: () async {
501+
MetroConsole.writeInGreen('[Command] ${name.snakeCase} created 🎉');
502+
});
503+
504+
// Add to custom_commands.json
505+
String commandJson = jsonEncode({
506+
"name": nameReCase.snakeCase,
507+
"category": category ?? "app",
508+
"script": "${nameReCase.snakeCase}.dart"
509+
});
510+
511+
try {
512+
File file = File(customCommandsFilePath);
513+
514+
String customCommandsFile = await loadAsset(customCommandsFilePath);
515+
516+
List<dynamic> commands = jsonDecode(customCommandsFile);
517+
if (!commands.any((command) => command["name"] == nameReCase.snakeCase)) {
518+
commands.add(jsonDecode(commandJson));
519+
}
520+
String updatedCommands = jsonEncode(commands);
521+
522+
// format json file
523+
String formattedJson = const JsonEncoder.withIndent(' ')
524+
.convert(jsonDecode(updatedCommands));
525+
526+
await file.writeAsString(formattedJson);
527+
} catch (e) {
528+
MetroConsole.writeInRed(
529+
'[Command] ${name.snakeCase} failed to create command: $e');
530+
return;
531+
}
532+
}
533+
447534
/// Creates a new Stateful Widget.
448535
static makeStatefulWidget(String className, String value,
449536
{String folderPath = widgetsFolder,
@@ -919,13 +1006,88 @@ final Map<Type, NyApiService> apiDecoders = {${reg.allMatches(file).map((e) => e
9191006
forceCreate: (hasForceFlag ?? false));
9201007
break;
9211008
}
1009+
case commandsFolder:
1010+
{
1011+
if (templateName.contains("_command")) {
1012+
templateName = templateName.replaceAll("_command", "");
1013+
}
1014+
String category = 'app';
1015+
if (template.options.containsKey('category')) {
1016+
category = template.options['category'];
1017+
}
1018+
await makeCommand(templateName, template.stub,
1019+
forceCreate: (hasForceFlag ?? false), category: category);
1020+
break;
1021+
}
9221022
default:
9231023
{
9241024
continue;
9251025
}
9261026
}
9271027
}
9281028
}
1029+
1030+
/// Discovers custom commands from a JSON file.
1031+
static Future<List<NyCommand>> discoverCustomCommands() async {
1032+
try {
1033+
final configFile = File('lib/app/commands/custom_commands.json');
1034+
if (await configFile.exists()) {
1035+
final jsonStr = await configFile.readAsString();
1036+
final List<dynamic> commandConfigs = jsonDecode(jsonStr);
1037+
1038+
// Remove any duplicate commands from commandConfigs
1039+
final Set<String> commandNames = {};
1040+
commandConfigs.removeWhere((config) {
1041+
final name = config['name'];
1042+
if (commandNames.contains(name)) {
1043+
return true; // Remove duplicate
1044+
} else {
1045+
commandNames.add(name);
1046+
return false; // Keep unique
1047+
}
1048+
});
1049+
1050+
// List of commands
1051+
List<NyCommand> allCommands = commandConfigs.map<NyCommand>((config) {
1052+
assert(config['name'] != null,
1053+
'Command "name" is required in custom_commands.json');
1054+
assert(config['script'] != null,
1055+
'Command "script" is required in custom_commands.json');
1056+
return NyCommand(
1057+
name: config['name'],
1058+
category:
1059+
config.containsKey('category') ? config['category'] : "app",
1060+
action: (args) => _executeCommandScript(config['script'], args),
1061+
);
1062+
}).toList();
1063+
1064+
// Sort commands by category
1065+
allCommands.sort((a, b) {
1066+
if (a.category == b.category) {
1067+
return (a.name ?? "").compareTo(b.name ?? "");
1068+
}
1069+
return (a.category ?? "").compareTo(b.category ?? "");
1070+
});
1071+
1072+
return allCommands;
1073+
}
1074+
} catch (e) {
1075+
MetroConsole.writeInRed(
1076+
'Error loading custom commands: $e\n\nMake sure to create a custom_commands.json file in the lib/app/commands directory.');
1077+
}
1078+
return [];
1079+
}
1080+
1081+
/// Executes a command script.
1082+
static Future<void> _executeCommandScript(
1083+
String scriptPath, List<String> args) async {
1084+
final script = File('lib/app/commands/$scriptPath');
1085+
if (await script.exists()) {
1086+
await runProcess('dart run ${script.path} ${args.join(' ')}');
1087+
} else {
1088+
MetroConsole.writeInRed('Command script not found: $scriptPath');
1089+
}
1090+
}
9291091
}
9301092

9311093
/// IterableExtension

lib/router/router.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1206,7 +1206,8 @@ class NyRouter {
12061206
///
12071207
/// [navigationType] can be assigned with the following:
12081208
/// NavigationType.push, NavigationType.pushReplace,
1209-
/// NavigationType.pushAndRemoveUntil or NavigationType.popAndPushNamed
1209+
/// NavigationType.pushAndRemoveUntil, NavigationType.popAndPushNamed or
1210+
/// NavigationType.pushAndForgetAll.
12101211
///
12111212
/// [transitionType] allows you to assign a transition type for when
12121213
/// navigating to the new route. E.g. [TransitionType.fade()] or

lib/widgets/ny_base_state.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ abstract class NyBaseState<T extends StatefulWidget> extends State<T> {
106106
return null;
107107
}
108108

109+
/// Check if the [queryParameters] contains a specific key.
110+
bool hasQueryParameter(String key) {
111+
final queryParametersData = queryParameters();
112+
if (queryParametersData == null) return false;
113+
if (queryParametersData is Map) {
114+
return queryParametersData.containsKey(key);
115+
}
116+
return false;
117+
}
118+
109119
/// When you call [updateState], this method will be called within your
110120
/// State. The [data] parameter will contain any data passed from the
111121
/// updateState method.

lib/widgets/ny_stateful_widget.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,20 @@ abstract class NyStatefulWidget<T extends BaseController>
5252
}
5353

5454
/// Returns query params
55-
dynamic queryParameters() {
55+
dynamic queryParameters({String? key}) {
5656
if (this.controller.request == null) {
5757
return null;
5858
}
59-
return this.controller.request!.queryParameters();
59+
return this.controller.request!.queryParameters(key: key);
60+
}
61+
62+
/// Check if the [queryParameters] contains a specific key.
63+
bool hasQueryParameter(String key) {
64+
final queryParametersData = queryParameters();
65+
if (queryParametersData == null) return false;
66+
if (queryParametersData is Map) {
67+
return queryParametersData.containsKey(key);
68+
}
69+
return false;
6070
}
6171
}

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: nylo_support
22
description: Support library for the Nylo framework. This library supports routing, widgets, localization, cli, storage and more.
3-
version: 6.24.0
3+
version: 6.25.0
44
homepage: https://nylo.dev
55
repository: https://github.com/nylo-core/support/tree/6.x
66
issue_tracker: https://github.com/nylo-core/support/issues

0 commit comments

Comments
 (0)