generate method

Future<void> generate(
  1. ProjectConfig config,
  2. String targetPath
)

Generates a new Flutter project according to config at targetPath.

Implementation

Future<void> generate(ProjectConfig config, String targetPath) async {
  final progress = _logger.progress(
      'Generating project "${config.projectName}" at $targetPath...');

  try {
    // 1. Run flutter create
    progress.update('Running flutter create...');
    final platformsString = config.platforms.join(',');
    final createResult = await Process.run('flutter', [
      'create',
      '--org',
      config.org,
      '--project-name',
      config.projectName.replaceAll('-', '_'),
      if (platformsString.isNotEmpty) ...['--platforms', platformsString],
      targetPath,
    ]);

    if (createResult.exitCode != 0) {
      progress.fail('Failed to run flutter create');
      _logger.error('Flutter create stderr:\n${createResult.stderr}');
      throw Exception('Flutter create failed');
    }

    final projectDir = Directory(targetPath);
    if (!projectDir.existsSync()) {
      progress.fail('Project directory was not created');
      throw Exception('Project directory was not created');
    }

    // 2. Clear out standard lib/ contents
    progress.update('Setting up architecture: ${config.architecture}...');
    final libDir = Directory(p.join(targetPath, 'lib'));
    if (libDir.existsSync()) {
      libDir.deleteSync(recursive: true);
    }
    libDir.createSync();

    // 3. Create architecture directories
    List<String> directories = [];

    switch (config.architecture) {
      case 'Clean Architecture':
        final smLower = config.stateManagement.toLowerCase();
        String stateFolder = 'bloc';
        if (smLower.contains('provider')) {
          stateFolder = 'providers';
        } else if (smLower.contains('cubit')) {
          stateFolder = 'cubit';
        } else if (smLower.contains('getx')) {
          stateFolder = 'controllers';
        } else if (smLower.contains('riverpod')) {
          stateFolder = 'providers';
        }

        directories = [
          'core/env',
          'core/routes',
          'core/theme',
          'core/errors',
          'core/network',
          'core/extensions',
          'core/services',
          'core/usecases',
          'core/utils',
          'shared/components',
          'shared/theme',
          'shared/validation',
          'di/modules',
          'features/auth/data/datasources',
          'features/auth/data/models',
          'features/auth/data/repositories',
          'features/auth/domain/entities',
          'features/auth/domain/repositories',
          'features/auth/domain/usecases',
          'features/auth/presentation/$stateFolder',
          'features/auth/presentation/pages',
          'features/auth/presentation/widgets',
        ];
        break;
      case 'MVVM':
        directories = [
          'core/errors',
          'core/network',
          'core/utils',
          'core/theme',
          'core/routes',
          'core/env',
          'models',
          'views',
          'viewmodels',
          'services',
          'utils',
          'injection',
        ];
        break;
      case 'MVC':
      default:
        directories = [
          'core/errors',
          'core/network',
          'core/utils',
          'core/theme',
          'core/routes',
          'core/env',
          'models',
          'views',
          'controllers',
          'services',
          'utils',
          'injection',
        ];
        break;
    }

    for (final dir in directories) {
      final dirFile = Directory(p.join(libDir.path, dir));
      dirFile.createSync(recursive: true);
      // Create .gitkeep so empty directories are tracked in source control
      File(p.join(dirFile.path, '.gitkeep')).createSync();
    }

    // 4. Generate main.dart
    final templateEngine = StringTemplateEngine();
    final variables = {
      'project_name': config.projectName,
      'project_name_pascal': _toPascalCase(config.projectName),
      'architecture': config.architecture,
      'state_management': config.stateManagement,
    };
    final mainTemplate =
        TemplateBundle.getTemplate('project/main.dart.stub') ?? '';
    final mainContent = templateEngine.render(mainTemplate, variables);
    File(p.join(libDir.path, 'main.dart')).writeAsStringSync(mainContent);

    // 5. Git initialization
    if (config.gitInit) {
      progress.update('Initializing Git repository...');
      await Process.run('git', ['init'], workingDirectory: targetPath);
    }

    // 5.5 Generate project.yaml
    progress.update('Generating project.yaml...');
    final configService = YamlConfigService();
    configService.setValue('project', config.toYamlMap());
    await configService.save(p.join(targetPath, 'project.yaml'));

    // 6. Generate README
    if (config.generateReadme) {
      progress.update('Generating README.md...');
      final readmeTemplate =
          TemplateBundle.getTemplate('project/README.md.stub') ?? '';
      final readmeContent = templateEngine.render(readmeTemplate, variables);
      File(p.join(targetPath, 'README.md')).writeAsStringSync(readmeContent);
    }

    // 6.5 Generate core assets
    progress.update('Generating core components & assets...');
    final originalDir = Directory.current;
    Directory.current = targetPath;
    try {
      final componentGenerator = ComponentGenerator(_logger, templateEngine);
      await componentGenerator.generateComponent('assets', 'assets');
      await componentGenerator.generateComponent('theme', 'theme');
      await componentGenerator.generateComponent('network', 'network');
      await componentGenerator.generateComponent('error', 'error');
      await componentGenerator.generateComponent('extensions', 'extensions');
      await componentGenerator.generateComponent('routes', 'routes');
    } finally {
      Directory.current = originalDir;
    }

    // 7. Add Dependencies
    progress.update('Adding selected dependencies...');
    final engine =
        DependencyEngine(pubspecPath: p.join(targetPath, 'pubspec.yaml'));
    final registry = PackageRegistry();

    final allDependencies = Set<String>.from(config.dependencies)
      ..addAll(['dio', 'pretty_dio_logger', 'go_router']);

    for (final package in allDependencies) {
      final version = registry.getVersion(package) ?? 'any';
      final isDev = package == 'build_runner' ||
          package == 'lints' ||
          package == 'flutter_lints' ||
          package.endsWith('_generator');
      await engine.addDependency(package, version, isDev: isDev);
    }

    // 8. Post-generation commands
    progress.update('Running flutter pub get...');
    final pubGetResult = await Process.run('flutter', ['pub', 'get'],
        workingDirectory: targetPath);
    if (pubGetResult.exitCode != 0) {
      _logger.error('flutter pub get failed:\n${pubGetResult.stderr}');
    }

    progress.update('Running dart format...');
    final formatResult = await Process.run('dart', ['format', '.'],
        workingDirectory: targetPath);
    if (formatResult.exitCode != 0) {
      _logger.error('dart format failed:\n${formatResult.stderr}');
    }

    progress
        .complete('Successfully generated project "${config.projectName}"!');
  } catch (e) {
    progress.fail('Failed to generate project "${config.projectName}"');
    rethrow;
  }
}