From b6e0a6e7c90e4c730cffd5c87f19e9e913ed7297 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Fri, 8 Apr 2022 17:05:06 -0500 Subject: [PATCH] build: generate_template script + workflow (#11) --- .github/workflows/generate_template.yaml | 44 ++++ analysis_options.yaml | 3 + brick/brick.yaml | 53 +++++ src/.gitignore | 48 ++++ src/LICENSE | 21 ++ src/README.md | 26 ++ .../example/android/app/build.gradle | 2 +- .../android/app/src/debug/AndroidManifest.xml | 2 +- .../android/app/src/main/AndroidManifest.xml | 2 +- .../my_plugin}/example/MainActivity.kt | 2 +- .../app/src/profile/AndroidManifest.xml | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 6 +- .../macos/Runner/Configs/AppInfo.xcconfig | 2 +- tool/generator/main.dart | 222 ++++++++++++++++++ tool/generator/pubspec.yaml | 9 + 15 files changed, 435 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/generate_template.yaml create mode 100644 analysis_options.yaml create mode 100644 brick/brick.yaml create mode 100644 src/.gitignore create mode 100644 src/LICENSE create mode 100644 src/README.md rename src/my_plugin/example/android/app/src/main/kotlin/{dev/flutter/plugins => com/example/my_plugin}/example/MainActivity.kt (71%) create mode 100644 tool/generator/main.dart create mode 100644 tool/generator/pubspec.yaml diff --git a/.github/workflows/generate_template.yaml b/.github/workflows/generate_template.yaml new file mode 100644 index 0000000..34506d6 --- /dev/null +++ b/.github/workflows/generate_template.yaml @@ -0,0 +1,44 @@ +name: generate_template + +on: + push: + paths: + - tool/generator/** + - app/** + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: dart-lang/setup-dart@v1 + + - name: Install Dependencies + working-directory: tool/generator + run: dart pub get + + - name: Generate Template + run: dart ./tool/generator/main.dart + + - name: Config Git User + run: | + git config user.name VGV Bot + git config user.email vgvbot@users.noreply.github.com + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v3.6.0 + with: + base: main + branch: chore/generate-template + commit-message: "chore: generate template" + title: "chore: generate template" + body: Please squash and merge me! + labels: bot + author: VGV Bot + assignees: vgvbot + reviewers: felangel + committer: VGV Bot diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..09c982f --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + exclude: + - brick/** diff --git a/brick/brick.yaml b/brick/brick.yaml new file mode 100644 index 0000000..20c4e69 --- /dev/null +++ b/brick/brick.yaml @@ -0,0 +1,53 @@ +name: very_good_flutter_plugin +description: A very good federated Flutter plugin. +version: 0.1.0+1 + +environment: + mason: ">=0.1.0-dev <0.1.0" + +vars: + project_name: + type: string + description: The name of the flutter plugin + default: my_plugin + prompt: What is the name of the plugin? + description: + type: string + description: A short description of the plugin + default: A very good plugin + prompt: Please enter the plugin description. + org_name: + type: string + description: The organization name + default: com.example.verygood.plugin + prompt: What is the organization name? + android: + type: boolean + description: Whether the plugin will support Android + default: true + prompt: Do you want to include Android support? + ios: + type: boolean + description: Whether the plugin will support iOS + default: true + prompt: Do you want to include iOS support? + web: + type: boolean + description: Whether the plugin will support Web + default: true + prompt: Do you want to include Web support? + linux: + type: boolean + description: Whether the plugin will support Linux + default: true + prompt: Do you want to include Linux support? + macos: + type: boolean + description: Whether the plugin will support MacOS + default: true + prompt: Do you want to include MacOS support? + windows: + type: boolean + description: Whether the plugin will support Windows + default: true + prompt: Do you want to include Windows support? diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..4aa0df8 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,48 @@ +.DS_Store +.atom/ +.idea/ +.vscode/ + +.packages +.pub/ +.dart_tool/ +pubspec.lock +flutter_export_environment.sh +coverage/ + +Podfile.lock +Pods/ +.symlinks/ +**/Flutter/App.framework/ +**/Flutter/ephemeral/ +**/Flutter/Flutter.podspec +**/Flutter/Flutter.framework/ +**/Flutter/Generated.xcconfig +**/Flutter/flutter_assets/ + +ServiceDefinitions.json +xcuserdata/ +**/DerivedData/ + +local.properties +keystore.properties +.gradle/ +gradlew +gradlew.bat +gradle-wrapper.jar +.flutter-plugins-dependencies +*.iml + +generated_plugin_registrant.cc +generated_plugin_registrant.h +generated_plugin_registrant.dart +GeneratedPluginRegistrant.java +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m +GeneratedPluginRegistrant.swift +build/ +.flutter-plugins + +.project +.classpath +.settings \ No newline at end of file diff --git a/src/LICENSE b/src/LICENSE new file mode 100644 index 0000000..bba8e50 --- /dev/null +++ b/src/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Very Good Ventures + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..6922fbb --- /dev/null +++ b/src/README.md @@ -0,0 +1,26 @@ +# my_plugin + +[![Very Good Ventures][logo_white]][very_good_ventures_link_dark] +[![Very Good Ventures][logo_black]][very_good_ventures_link_light] + +Developed with 💙 by [Very Good Ventures][very_good_ventures_link] 🦄 + +![coverage][coverage_badge] +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +A Very Good Flutter Federated Plugin created by the [Very Good Ventures Team][very_good_ventures_link]. + +Generated by the [Very Good CLI][very_good_cli_link] 🤖 + +[coverage_badge]: app/coverage_badge.svg +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only +[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis +[very_good_cli_link]: https://github.com/VeryGoodOpenSource/very_good_cli +[very_good_ventures_link]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core +[very_good_ventures_link_dark]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core#gh-dark-mode-only +[very_good_ventures_link_light]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core#gh-light-mode-only diff --git a/src/my_plugin/example/android/app/build.gradle b/src/my_plugin/example/android/app/build.gradle index f77bd87..eaf4085 100644 --- a/src/my_plugin/example/android/app/build.gradle +++ b/src/my_plugin/example/android/app/build.gradle @@ -43,7 +43,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "dev.flutter.plugins.example" + applicationId "com.example.my_plugin.example" minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() diff --git a/src/my_plugin/example/android/app/src/debug/AndroidManifest.xml b/src/my_plugin/example/android/app/src/debug/AndroidManifest.xml index 58e7dac..ee5028d 100644 --- a/src/my_plugin/example/android/app/src/debug/AndroidManifest.xml +++ b/src/my_plugin/example/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="com.example.my_plugin.example"> diff --git a/src/my_plugin/example/android/app/src/main/AndroidManifest.xml b/src/my_plugin/example/android/app/src/main/AndroidManifest.xml index 6f804c2..715e137 100644 --- a/src/my_plugin/example/android/app/src/main/AndroidManifest.xml +++ b/src/my_plugin/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="com.example.my_plugin.example"> + package="com.example.my_plugin"> diff --git a/src/my_plugin/example/ios/Runner.xcodeproj/project.pbxproj b/src/my_plugin/example/ios/Runner.xcodeproj/project.pbxproj index f2f0ce9..f0896d1 100644 --- a/src/my_plugin/example/ios/Runner.xcodeproj/project.pbxproj +++ b/src/my_plugin/example/ios/Runner.xcodeproj/project.pbxproj @@ -363,7 +363,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.example; + PRODUCT_BUNDLE_IDENTIFIER = com.example.my_plugin; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -492,7 +492,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.example; + PRODUCT_BUNDLE_IDENTIFIER = com.example.my_plugin; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -515,7 +515,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.example; + PRODUCT_BUNDLE_IDENTIFIER = com.example.my_plugin; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/src/my_plugin/example/macos/Runner/Configs/AppInfo.xcconfig b/src/my_plugin/example/macos/Runner/Configs/AppInfo.xcconfig index 3c916de..1f81d9d 100644 --- a/src/my_plugin/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/src/my_plugin/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.example +PRODUCT_BUNDLE_IDENTIFIER = com.example.my_plugin // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2022 dev.flutter.plugins. All rights reserved. diff --git a/tool/generator/main.dart b/tool/generator/main.dart new file mode 100644 index 0000000..41f1f11 --- /dev/null +++ b/tool/generator/main.dart @@ -0,0 +1,222 @@ +import 'dart:io'; +import 'package:path/path.dart' as path; + +final _githubPath = path.join('.github'); +final _sourcePath = path.join('src'); +final _targetPath = path.join('brick', '__brick__'); +final _androidPath = path.join(_targetPath, 'my_plugin_android', 'android'); +final _androidKotlinPath = path.join(_androidPath, 'src', 'main', 'kotlin'); +final _sourceMyPluginKtPath = path.join( + _androidKotlinPath, + 'com', + 'example', + 'my_plugin', + 'MyPluginPlugin.kt', +); +final _targetMyPluginKtPath = path.join( + _androidKotlinPath, + '{{#pathCase}}{{org_name}}{{/pathCase}}', + '{{#pascalCase}}{{project_name}}{{/pascalCase}}Plugin.kt', +); +final year = DateTime.now().year; +final copyrightHeader = ''' +// Copyright (c) $year, Very Good Ventures +// https://verygood.ventures +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. +'''; + +final excludedFiles = [ + path.join( + _targetPath, + '.github', + 'workflows', + 'generate_template.yaml', + ), + path.join(_targetPath, '.github', 'CODEOWNERS'), +]; + +void main() async { + // Remove Previously Generated Files + final targetDir = Directory(_targetPath); + if (targetDir.existsSync()) { + await targetDir.delete(recursive: true); + } + + // Copy Project Files + await Future.wait([ + Shell.cp(_sourcePath, _targetPath), + Shell.cp(_githubPath, path.join(_targetPath)), + () async { + await Shell.mkdir(File(_targetMyPluginKtPath).parent.path); + await Shell.cp(_sourceMyPluginKtPath, _targetMyPluginKtPath); + await Shell.rm(File(_sourceMyPluginKtPath).parent.parent.path); + }() + ]); + + // Remove excluded files + await Future.wait( + excludedFiles.map((file) => File(file).delete(recursive: true)), + ); + + await Future.wait( + Directory(_targetPath) + .listSync(recursive: true) + .whereType() + .map((_) async { + var file = _; + if (!file.existsSync()) return; + + // Add copyright header to all .dart files + if (path.extension(file.path) == '.dart') { + final contents = await file.readAsString(); + file = await file.writeAsString('$copyrightHeader\n$contents'); + } + + // Template File Contents + final contents = + file.isAsset() ? await file.readAsBytes() : await file.readAsString(); + final templatedContents = (contents is String) + ? contents + .replaceAll( + 'com.example.my_plugin', + '{{#dotCase}}{{org_name}}{{/dotCase}}', + ) + .replaceAll( + 'my_plugin', + '{{#snakeCase}}{{project_name}}{{/snakeCase}}', + ) + .replaceAll( + 'my-plugin', + '{{#paramCase}}{{project_name}}{{/paramCase}}', + ) + .replaceAll( + 'MyPlugin', + '{{#pascalCase}}{{project_name}}{{/pascalCase}}', + ) + .replaceAll( + 'A very good Flutter federated plugin', + '{{{description}}}', + ) + : contents; + file = templatedContents is String + ? await file.writeAsString(templatedContents) + : await file.writeAsBytes(templatedContents as List); + + /// Template file paths + final fileSegments = file.path.split('/').sublist(2); + if (fileSegments + .any((e) => e.contains('my_plugin') || e.contains('MyPlugin'))) { + final newSegments = fileSegments.map((e) { + return e + .replaceAll( + 'MyPlugin', + '{{#pascalCase}}{{project_name}}{{/pascalCase}}', + ) + .replaceAll( + 'my_plugin', + '{{#snakeCase}}{{project_name}}{{/snakeCase}}', + ); + }); + final newPathSegment = newSegments.join('/'); + final newPath = path.join(_targetPath, newPathSegment); + final newFile = File(newPath)..createSync(recursive: true); + templatedContents is String + ? newFile.writeAsStringSync(templatedContents) + : newFile.writeAsBytesSync(templatedContents as List); + file = newFile; + } + }), + ); + + // Clean up top-level directories + const topLevelDirs = [ + 'my_plugin', + 'my_plugin_android', + 'my_plugin_ios', + 'my_plugin_linux', + 'my_plugin_macos', + 'my_plugin_platform_interface', + 'my_plugin_web', + 'my_plugin_windows', + ]; + for (final dir in topLevelDirs) { + Directory(path.join(_targetPath, dir)).deleteSync(recursive: true); + } +} + +class Shell { + static Future cp(String source, String destination) { + return _Cmd.run('cp', ['-rf', source, destination]); + } + + static Future rm(String source) { + return _Cmd.run('rm', ['-rf', source]); + } + + static Future mkdir(String destination) { + return _Cmd.run('mkdir', ['-p', destination]); + } +} + +class _Cmd { + static Future run( + String cmd, + List args, { + bool throwOnError = true, + String? processWorkingDir, + }) async { + final result = await Process.run(cmd, args, + workingDirectory: processWorkingDir, runInShell: true); + + if (throwOnError) { + _throwIfProcessFailed(result, cmd, args); + } + return result; + } + + static void _throwIfProcessFailed( + ProcessResult pr, + String process, + List args, + ) { + if (pr.exitCode != 0) { + final values = { + 'Standard out': pr.stdout.toString().trim(), + 'Standard error': pr.stderr.toString().trim() + }..removeWhere((k, v) => v.isEmpty); + + String message; + if (values.isEmpty) { + message = 'Unknown error'; + } else if (values.length == 1) { + message = values.values.single; + } else { + message = values.entries.map((e) => '${e.key}\n${e.value}').join('\n'); + } + + throw ProcessException(process, args, message, pr.exitCode); + } + } +} + +extension on File { + bool isAsset() { + const extensions = { + '.png', + '.ico', + '.svg', + '.jpg', + '.jpeg', + '.mov', + '.mp4', + 'mp3', + '.wav', + '.ttf' + }; + final ext = path.extension(this.path); + return extensions.contains(ext); + } +} diff --git a/tool/generator/pubspec.yaml b/tool/generator/pubspec.yaml new file mode 100644 index 0000000..09eee29 --- /dev/null +++ b/tool/generator/pubspec.yaml @@ -0,0 +1,9 @@ +name: generator +description: A template generator for Very Good Flutter Plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + path: ^1.8.0