create-mobile-arch 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/prompts.js ADDED
@@ -0,0 +1,140 @@
1
+ const inquirer = require('inquirer');
2
+ const chalk = require('chalk');
3
+
4
+ /**
5
+ * Prompts the user for project configuration choices
6
+ * @returns {Promise<Object>} User's answers
7
+ */
8
+ async function promptUserChoices() {
9
+ console.log('\n');
10
+ console.log(chalk.cyan.bold('⚙️ Configure your Flutter project'));
11
+ console.log(chalk.gray('Answer the following questions to customize your project setup:'));
12
+ console.log('\n');
13
+
14
+ const questions = [
15
+ {
16
+ type: 'list',
17
+ name: 'architecture',
18
+ message: 'Select your preferred architecture pattern:',
19
+ choices: [
20
+ {
21
+ name: 'Clean Architecture (Domain-driven with layers)',
22
+ value: 'clean',
23
+ short: 'Clean Architecture'
24
+ },
25
+ {
26
+ name: 'Feature First (Organized by features)',
27
+ value: 'feature-first',
28
+ short: 'Feature First'
29
+ }
30
+ ],
31
+ default: 'clean'
32
+ },
33
+ {
34
+ type: 'list',
35
+ name: 'stateManagement',
36
+ message: 'Choose your state management solution:',
37
+ choices: [
38
+ {
39
+ name: 'Riverpod (Recommended - compile-safe, testable)',
40
+ value: 'riverpod',
41
+ short: 'Riverpod'
42
+ },
43
+ {
44
+ name: 'Bloc (Event-driven state management)',
45
+ value: 'bloc',
46
+ short: 'Bloc'
47
+ }
48
+ ],
49
+ default: 'riverpod'
50
+ },
51
+ {
52
+ type: 'list',
53
+ name: 'backend',
54
+ message: 'Select your backend integration:',
55
+ choices: [
56
+ {
57
+ name: 'Firebase (Auth, Firestore, Storage)',
58
+ value: 'firebase',
59
+ short: 'Firebase'
60
+ },
61
+ {
62
+ name: 'REST API (Custom backend with HTTP)',
63
+ value: 'rest',
64
+ short: 'REST API'
65
+ }
66
+ ],
67
+ default: 'rest'
68
+ },
69
+ {
70
+ type: 'confirm',
71
+ name: 'includeExamples',
72
+ message: 'Include example code and sample features?',
73
+ default: true
74
+ }
75
+ ];
76
+
77
+ try {
78
+ const answers = await inquirer.prompt(questions);
79
+ return answers;
80
+ } catch (error) {
81
+ if (error.isTtyError) {
82
+ throw new Error('Prompt could not be rendered in this environment');
83
+ } else {
84
+ throw new Error(`Failed to get user input: ${error.message}`);
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Confirms before overwriting an existing directory
91
+ * @param {string} projectName - Name of the existing project
92
+ * @returns {Promise<boolean>} User's confirmation
93
+ */
94
+ async function confirmOverwrite(projectName) {
95
+ console.log('\n');
96
+ console.log(chalk.yellow.bold('⚠️ Warning:'), `Directory "${projectName}" already exists!`);
97
+
98
+ const { confirm } = await inquirer.prompt([
99
+ {
100
+ type: 'confirm',
101
+ name: 'confirm',
102
+ message: 'Do you want to overwrite it?',
103
+ default: false
104
+ }
105
+ ]);
106
+
107
+ return confirm;
108
+ }
109
+
110
+ /**
111
+ * Displays a summary of user choices before generation
112
+ * @param {string} projectName - Project name
113
+ * @param {Object} choices - User's configuration choices
114
+ */
115
+ function displayConfigSummary(projectName, choices) {
116
+ console.log('\n');
117
+ console.log(chalk.cyan.bold('📋 Project Configuration Summary:'));
118
+ console.log(chalk.gray('─'.repeat(50)));
119
+ console.log(chalk.white(' Project Name: '), chalk.green(projectName));
120
+ console.log(chalk.white(' Architecture: '), chalk.green(
121
+ choices.architecture === 'clean' ? 'Clean Architecture' : 'Feature First'
122
+ ));
123
+ console.log(chalk.white(' State Management: '), chalk.green(
124
+ choices.stateManagement === 'riverpod' ? 'Riverpod' : 'Bloc'
125
+ ));
126
+ console.log(chalk.white(' Backend: '), chalk.green(
127
+ choices.backend === 'firebase' ? 'Firebase' : 'REST API'
128
+ ));
129
+ console.log(chalk.white(' Include Examples: '), chalk.green(
130
+ choices.includeExamples ? 'Yes' : 'No'
131
+ ));
132
+ console.log(chalk.gray('─'.repeat(50)));
133
+ console.log('\n');
134
+ }
135
+
136
+ module.exports = {
137
+ promptUserChoices,
138
+ confirmOverwrite,
139
+ displayConfigSummary
140
+ };
package/src/utils.js ADDED
@@ -0,0 +1,185 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+
5
+ /**
6
+ * Validates the project name according to Flutter naming conventions
7
+ * @param {string} name - Project name to validate
8
+ * @returns {Object} - { valid: boolean, error: string }
9
+ */
10
+ function validateProjectName(name) {
11
+ if (!name) {
12
+ return {
13
+ valid: false,
14
+ error: 'Project name is required'
15
+ };
16
+ }
17
+
18
+ // Flutter project names must be valid Dart package names
19
+ // Must be lowercase, can contain underscores and numbers, but not start with a number
20
+ const validNamePattern = /^[a-z][a-z0-9_]*$/;
21
+
22
+ if (!validNamePattern.test(name)) {
23
+ return {
24
+ valid: false,
25
+ error: 'Project name must start with a lowercase letter and can only contain lowercase letters, numbers, and underscores'
26
+ };
27
+ }
28
+
29
+ // Reserved Dart keywords
30
+ const reservedWords = [
31
+ 'abstract', 'as', 'assert', 'async', 'await', 'break', 'case', 'catch',
32
+ 'class', 'const', 'continue', 'default', 'do', 'else', 'enum', 'export',
33
+ 'extends', 'false', 'final', 'finally', 'for', 'if', 'import', 'in',
34
+ 'is', 'library', 'new', 'null', 'operator', 'part', 'return', 'super',
35
+ 'switch', 'this', 'throw', 'true', 'try', 'var', 'void', 'while', 'with'
36
+ ];
37
+
38
+ if (reservedWords.includes(name)) {
39
+ return {
40
+ valid: false,
41
+ error: `Project name "${name}" is a reserved Dart keyword`
42
+ };
43
+ }
44
+
45
+ return { valid: true };
46
+ }
47
+
48
+ /**
49
+ * Checks if a directory exists
50
+ * @param {string} dirPath - Directory path to check
51
+ * @returns {boolean}
52
+ */
53
+ async function directoryExists(dirPath) {
54
+ try {
55
+ const stats = await fs.stat(dirPath);
56
+ return stats.isDirectory();
57
+ } catch (error) {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Converts a string to PascalCase (for app names)
64
+ * @param {string} str - String to convert
65
+ * @returns {string}
66
+ */
67
+ function toPascalCase(str) {
68
+ return str
69
+ .split('_')
70
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
71
+ .join('');
72
+ }
73
+
74
+ /**
75
+ * Converts a string to snake_case
76
+ * @param {string} str - String to convert
77
+ * @returns {string}
78
+ */
79
+ function toSnakeCase(str) {
80
+ return str
81
+ .toLowerCase()
82
+ .replace(/[^a-z0-9]/g, '_')
83
+ .replace(/_+/g, '_')
84
+ .replace(/^_|_$/g, '');
85
+ }
86
+
87
+ /**
88
+ * Recursively copies directory and replaces placeholders in file contents
89
+ * @param {string} srcDir - Source directory
90
+ * @param {string} destDir - Destination directory
91
+ * @param {Object} replacements - Key-value pairs for placeholder replacement
92
+ */
93
+ async function copyAndReplace(srcDir, destDir, replacements) {
94
+ try {
95
+ // Ensure destination directory exists
96
+ await fs.ensureDir(destDir);
97
+
98
+ // Read all items in source directory
99
+ const items = await fs.readdir(srcDir);
100
+
101
+ for (const item of items) {
102
+ const srcPath = path.join(srcDir, item);
103
+ const destPath = path.join(destDir, item);
104
+
105
+ const stats = await fs.stat(srcPath);
106
+
107
+ if (stats.isDirectory()) {
108
+ // Recursively copy subdirectories
109
+ await copyAndReplace(srcPath, destPath, replacements);
110
+ } else if (stats.isFile()) {
111
+ // Read file content
112
+ let content = await fs.readFile(srcPath, 'utf8');
113
+
114
+ // Replace all placeholders
115
+ Object.keys(replacements).forEach(placeholder => {
116
+ const regex = new RegExp(placeholder, 'g');
117
+ content = content.replace(regex, replacements[placeholder]);
118
+ });
119
+
120
+ // Write to destination
121
+ await fs.writeFile(destPath, content, 'utf8');
122
+ }
123
+ }
124
+ } catch (error) {
125
+ throw new Error(`Failed to copy template: ${error.message}`);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Checks if Flutter is installed on the system
131
+ * @returns {boolean}
132
+ */
133
+ async function isFlutterInstalled() {
134
+ const { exec } = require('child_process');
135
+ const util = require('util');
136
+ const execPromise = util.promisify(exec);
137
+
138
+ try {
139
+ await execPromise('flutter --version');
140
+ return true;
141
+ } catch (error) {
142
+ return false;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Prints a success message with next steps
148
+ * @param {string} projectName - Name of the created project
149
+ * @param {string} projectPath - Path to the project
150
+ */
151
+ function printSuccessMessage(projectName, projectPath) {
152
+ console.log('\n');
153
+ console.log(chalk.green.bold('✅ Success!'), `Your Flutter project "${projectName}" has been created!`);
154
+ console.log('\n');
155
+ console.log(chalk.cyan('📁 Project location:'), chalk.white(projectPath));
156
+ console.log('\n');
157
+ console.log(chalk.yellow.bold('Next steps:'));
158
+ console.log(chalk.white(' 1.'), `cd ${projectName}`);
159
+ console.log(chalk.white(' 2.'), 'Open the project in your IDE (VS Code, Android Studio, etc.)');
160
+ console.log(chalk.white(' 3.'), 'Run:', chalk.cyan('flutter run'));
161
+ console.log('\n');
162
+ console.log(chalk.gray('💡 Tip: Check the README.md file in your project for architecture details'));
163
+ console.log('\n');
164
+ }
165
+
166
+ /**
167
+ * Prints an error message
168
+ * @param {string} message - Error message
169
+ */
170
+ function printError(message) {
171
+ console.log('\n');
172
+ console.log(chalk.red.bold('❌ Error:'), chalk.white(message));
173
+ console.log('\n');
174
+ }
175
+
176
+ module.exports = {
177
+ validateProjectName,
178
+ directoryExists,
179
+ toPascalCase,
180
+ toSnakeCase,
181
+ copyAndReplace,
182
+ isFlutterInstalled,
183
+ printSuccessMessage,
184
+ printError
185
+ };
@@ -0,0 +1,46 @@
1
+ include: package:flutter_lints/flutter.yaml
2
+
3
+ linter:
4
+ rules:
5
+ # Error rules
6
+ - avoid_empty_else
7
+ - avoid_relative_lib_imports
8
+ - avoid_types_as_parameter_names
9
+ - no_duplicate_case_values
10
+ - prefer_void_to_null
11
+ - valid_regexps
12
+
13
+ # Style rules
14
+ - always_declare_return_types
15
+ - always_require_non_null_named_parameters
16
+ - annotate_overrides
17
+ - avoid_init_to_null
18
+ - avoid_return_types_on_setters
19
+ - camel_case_extensions
20
+ - camel_case_types
21
+ - constant_identifier_names
22
+ - curly_braces_in_flow_control_structures
23
+ - empty_catches
24
+ - empty_constructor_bodies
25
+ - library_names
26
+ - library_prefixes
27
+ - prefer_contains
28
+ - prefer_final_fields
29
+ - prefer_is_empty
30
+ - prefer_is_not_empty
31
+ - prefer_single_quotes
32
+ - slash_for_doc_comments
33
+ - unnecessary_const
34
+ - unnecessary_new
35
+ - unnecessary_this
36
+ - use_rethrow_when_possible
37
+
38
+ analyzer:
39
+ exclude:
40
+ - "**/*.g.dart"
41
+ - "**/*.freezed.dart"
42
+ - "**/*.config.dart"
43
+ - "build/**"
44
+
45
+ errors:
46
+ invalid_annotation_target: ignore
@@ -0,0 +1,26 @@
1
+ /// Application-wide constants
2
+ class AppConstants {
3
+ // Private constructor to prevent instantiation
4
+ AppConstants._();
5
+
6
+ /// App Information
7
+ static const String appName = '__APP_NAME_PASCAL__';
8
+ static const String appVersion = '1.0.0';
9
+
10
+ /// API Configuration
11
+ static const String baseUrl = 'https://api.example.com';
12
+ static const String apiVersion = 'v1';
13
+ static const Duration apiTimeout = Duration(seconds: 30);
14
+
15
+ /// Storage Keys
16
+ static const String tokenKey = 'auth_token';
17
+ static const String userKey = 'user_data';
18
+ static const String themeKey = 'theme_mode';
19
+
20
+ /// Pagination
21
+ static const int defaultPageSize = 20;
22
+
23
+ /// Validation
24
+ static const int minPasswordLength = 8;
25
+ static const int maxUsernameLength = 30;
26
+ }
@@ -0,0 +1,90 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ /// Application theme configuration
4
+ /// Defines light and dark theme for the app
5
+ class AppTheme {
6
+ // Private constructor to prevent instantiation
7
+ AppTheme._();
8
+
9
+ // Color palette
10
+ static const Color primaryColor = Color(0xFF2196F3);
11
+ static const Color secondaryColor = Color(0xFF03DAC6);
12
+ static const Color errorColor = Color(0xFFB00020);
13
+ static const Color backgroundColor = Color(0xFFF5F5F5);
14
+
15
+ /// Light theme configuration
16
+ static ThemeData get lightTheme {
17
+ return ThemeData(
18
+ useMaterial3: true,
19
+ brightness: Brightness.light,
20
+ primaryColor: primaryColor,
21
+ scaffoldBackgroundColor: backgroundColor,
22
+ colorScheme: const ColorScheme.light(
23
+ primary: primaryColor,
24
+ secondary: secondaryColor,
25
+ error: errorColor,
26
+ background: backgroundColor,
27
+ ),
28
+ appBarTheme: const AppBarTheme(
29
+ centerTitle: true,
30
+ elevation: 0,
31
+ backgroundColor: primaryColor,
32
+ foregroundColor: Colors.white,
33
+ ),
34
+ elevatedButtonTheme: ElevatedButtonThemeData(
35
+ style: ElevatedButton.styleFrom(
36
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
37
+ shape: RoundedRectangleBorder(
38
+ borderRadius: BorderRadius.circular(8),
39
+ ),
40
+ ),
41
+ ),
42
+ inputDecorationTheme: InputDecorationTheme(
43
+ border: OutlineInputBorder(
44
+ borderRadius: BorderRadius.circular(8),
45
+ ),
46
+ contentPadding: const EdgeInsets.symmetric(
47
+ horizontal: 16,
48
+ vertical: 12,
49
+ ),
50
+ ),
51
+ );
52
+ }
53
+
54
+ /// Dark theme configuration
55
+ static ThemeData get darkTheme {
56
+ return ThemeData(
57
+ useMaterial3: true,
58
+ brightness: Brightness.dark,
59
+ primaryColor: primaryColor,
60
+ scaffoldBackgroundColor: const Color(0xFF121212),
61
+ colorScheme: const ColorScheme.dark(
62
+ primary: primaryColor,
63
+ secondary: secondaryColor,
64
+ error: errorColor,
65
+ ),
66
+ appBarTheme: const AppBarTheme(
67
+ centerTitle: true,
68
+ elevation: 0,
69
+ backgroundColor: Color(0xFF1F1F1F),
70
+ ),
71
+ elevatedButtonTheme: ElevatedButtonThemeData(
72
+ style: ElevatedButton.styleFrom(
73
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
74
+ shape: RoundedRectangleBorder(
75
+ borderRadius: BorderRadius.circular(8),
76
+ ),
77
+ ),
78
+ ),
79
+ inputDecorationTheme: InputDecorationTheme(
80
+ border: OutlineInputBorder(
81
+ borderRadius: BorderRadius.circular(8),
82
+ ),
83
+ contentPadding: const EdgeInsets.symmetric(
84
+ horizontal: 16,
85
+ vertical: 12,
86
+ ),
87
+ ),
88
+ );
89
+ }
90
+ }
@@ -0,0 +1,29 @@
1
+ /// Common utility functions for the application
2
+ class AppUtils {
3
+ // Private constructor to prevent instantiation
4
+ AppUtils._();
5
+
6
+ /// Validates email format
7
+ static bool isValidEmail(String email) {
8
+ final emailRegex = RegExp(
9
+ r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
10
+ );
11
+ return emailRegex.hasMatch(email);
12
+ }
13
+
14
+ /// Validates password strength
15
+ static bool isValidPassword(String password) {
16
+ return password.length >= 8;
17
+ }
18
+
19
+ /// Formats date to readable string
20
+ static String formatDate(DateTime date) {
21
+ return '${date.day}/${date.month}/${date.year}';
22
+ }
23
+
24
+ /// Capitalizes first letter of string
25
+ static String capitalize(String text) {
26
+ if (text.isEmpty) return text;
27
+ return text[0].toUpperCase() + text.substring(1);
28
+ }
29
+ }
@@ -0,0 +1,57 @@
1
+ import '../domain/entities/user.dart';
2
+
3
+ /// Data model for User
4
+ /// Models are responsible for JSON serialization/deserialization
5
+ class UserModel {
6
+ final String id;
7
+ final String name;
8
+ final String email;
9
+ final String createdAt;
10
+
11
+ UserModel({
12
+ required this.id,
13
+ required this.name,
14
+ required this.email,
15
+ required this.createdAt,
16
+ });
17
+
18
+ /// Convert JSON to UserModel
19
+ factory UserModel.fromJson(Map<String, dynamic> json) {
20
+ return UserModel(
21
+ id: json['id'] as String,
22
+ name: json['name'] as String,
23
+ email: json['email'] as String,
24
+ createdAt: json['created_at'] as String,
25
+ );
26
+ }
27
+
28
+ /// Convert UserModel to JSON
29
+ Map<String, dynamic> toJson() {
30
+ return {
31
+ 'id': id,
32
+ 'name': name,
33
+ 'email': email,
34
+ 'created_at': createdAt,
35
+ };
36
+ }
37
+
38
+ /// Convert UserModel to domain entity
39
+ User toEntity() {
40
+ return User(
41
+ id: id,
42
+ name: name,
43
+ email: email,
44
+ createdAt: DateTime.parse(createdAt),
45
+ );
46
+ }
47
+
48
+ /// Create UserModel from domain entity
49
+ factory UserModel.fromEntity(User user) {
50
+ return UserModel(
51
+ id: user.id,
52
+ name: user.name,
53
+ email: user.email,
54
+ createdAt: user.createdAt.toIso8601String(),
55
+ );
56
+ }
57
+ }
@@ -0,0 +1,23 @@
1
+ import 'package:equatable/equatable.dart';
2
+
3
+ /// Example entity representing a user in the domain layer
4
+ /// Entities are plain Dart classes with business logic
5
+ class User extends Equatable {
6
+ final String id;
7
+ final String name;
8
+ final String email;
9
+ final DateTime createdAt;
10
+
11
+ const User({
12
+ required this.id,
13
+ required this.name,
14
+ required this.email,
15
+ required this.createdAt,
16
+ });
17
+
18
+ @override
19
+ List<Object?> get props => [id, name, email, createdAt];
20
+
21
+ @override
22
+ String toString() => 'User(id: $id, name: $name, email: $email)';
23
+ }
@@ -0,0 +1,33 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+ import 'core/theme/app_theme.dart';
4
+ import 'presentation/screens/home_screen.dart';
5
+
6
+ void main() async {
7
+ WidgetsFlutterBinding.ensureInitialized();
8
+
9
+ // Initialize dependencies here
10
+ // await setupDependencyInjection();
11
+
12
+ runApp(
13
+ const ProviderScope(
14
+ child: MyApp(),
15
+ ),
16
+ );
17
+ }
18
+
19
+ class MyApp extends StatelessWidget {
20
+ const MyApp({super.key});
21
+
22
+ @override
23
+ Widget build(BuildContext context) {
24
+ return MaterialApp(
25
+ title: '__APP_NAME_PASCAL__',
26
+ debugShowCheckedModeBanner: false,
27
+ theme: AppTheme.lightTheme,
28
+ darkTheme: AppTheme.darkTheme,
29
+ themeMode: ThemeMode.system,
30
+ home: const HomeScreen(),
31
+ );
32
+ }
33
+ }