diff --git a/CoroutineTemplate/buildSrc/src/main/java/Versions.kt b/CoroutineTemplate/buildSrc/src/main/java/Versions.kt index 9c7f5ebd8..77cd0c998 100644 --- a/CoroutineTemplate/buildSrc/src/main/java/Versions.kt +++ b/CoroutineTemplate/buildSrc/src/main/java/Versions.kt @@ -6,7 +6,7 @@ object Versions { const val ANDROID_TARGET_SDK_VERSION = 30 const val ANDROID_VERSION_CODE = 1 - const val ANDROID_VERSION_NAME = "3.7.0" + const val ANDROID_VERSION_NAME = "3.8.0" // Dependencies (Alphabet sorted) const val ANDROID_COMMON_KTX_VERSION = "0.1.1" diff --git a/Gemfile.lock b/Gemfile.lock index 31f81edd0..448a53296 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -59,7 +59,7 @@ GEM faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - git (1.9.1) + git (1.11.0) rchardet (~> 1.8) kramdown (2.3.1) rexml @@ -68,9 +68,9 @@ GEM multipart-post (2.1.1) nap (1.1.0) no_proxy_fix (0.1.2) - nokogiri (1.13.4-x86_64-darwin) + nokogiri (1.13.6-x86_64-darwin) racc (~> 1.4) - nokogiri (1.13.4-x86_64-linux) + nokogiri (1.13.6-x86_64-linux) racc (~> 1.4) octokit (4.21.0) faraday (>= 0.9) diff --git a/RxJavaTemplate[DEPRECATED]/buildSrc/src/main/java/Versions.kt b/RxJavaTemplate[DEPRECATED]/buildSrc/src/main/java/Versions.kt index 46d780f22..49ce87e7e 100644 --- a/RxJavaTemplate[DEPRECATED]/buildSrc/src/main/java/Versions.kt +++ b/RxJavaTemplate[DEPRECATED]/buildSrc/src/main/java/Versions.kt @@ -2,7 +2,7 @@ object Versions { const val BUILD_GRADLE_VERSION = "4.2.2" const val ANDROID_VERSION_CODE = 9 - const val ANDROID_VERSION_NAME = "3.7.0" + const val ANDROID_VERSION_NAME = "3.8.0" const val ANDROID_MIN_SDK_VERSION = 23 const val ANDROID_TARGET_SDK_VERSION = 28 const val ANDROID_COMPILE_SDK_VERSION = 28 diff --git a/scripts/new_project.kts b/scripts/new_project.kts new file mode 100644 index 000000000..1d9a8bfc2 --- /dev/null +++ b/scripts/new_project.kts @@ -0,0 +1,230 @@ +import java.io.File + +object NewProject { + + private const val ARGUMENT_DELIMITER = "=" + private const val DOT_SEPARATOR = "." + private const val KEY_APP_NAME = "app-name" + private const val KEY_PACKAGE_NAME = "package-name" + private const val TEMPLATE_APP_NAME = "Coroutine Template" + private const val TEMPLATE_APPLICATION_CLASS_NAME = "CoroutineTemplateApplication" + private const val TEMPLATE_FOLDER_NAME = "CoroutineTemplate" + private const val TEMPLATE_PACKAGE_NAME = "co.nimblehq.coroutine" + + private const val PATTERN_APP = "^[A-Z][a-zA-Z0-9\\s]*$" + private const val PATTERN_PACKAGE = "^[a-z]+(\\.[a-z][a-z0-9]*)+$" + + private val modules = listOf("app", "data", "domain") + private val fileSeparator = File.separator + + private var appName = "" + private var packageName = "" + + private val appNameWithoutSpace: String + get() = appName.replace(" ", "") + + private val applicationClassName: String + get() = "${appNameWithoutSpace}Application" + + private val projectPath: String + get() = rootPath + appNameWithoutSpace + + private val rootPath: String + get() = System.getProperty("user.dir").replace("scripts", "") + + fun generate(args: Array) { + handleArguments(args) + initializeNewProjectFolder() + cleanNewProjectFolder() + renamePackageNameFolders() + renamePackageNameWithinFiles() + renameApplicationClass() + renameAppName() + buildProjectAndRunTests() + } + + private fun handleArguments(args: Array) { + val argumentError = "ERROR: Invalid argument name: Ensure define arguments => app-name={\"MyProject\"} or {\"My Project\"} package-name={com.sample.myproject}" + when (args.size) { + 1 -> when { + args.first().startsWith(KEY_APP_NAME) -> showErrorMessage("ERROR: No package name has been provided") + args.first().startsWith(KEY_PACKAGE_NAME) -> showErrorMessage("ERROR: No app name has been provided") + else -> showErrorMessage(argumentError) + } + 2 -> args.forEach { arg -> + val (key, value) = arg.split(ARGUMENT_DELIMITER) + when (key) { + KEY_APP_NAME -> validateAppName(value) + KEY_PACKAGE_NAME -> validatePackageName(value) + else -> showErrorMessage(argumentError) + } + } + else -> showErrorMessage("ERROR: Require app-name and package-name to initialize the new project") + } + } + + private fun validateAppName(value: String) { + if (PATTERN_APP.toRegex().matches(value)) { + appName = value + } else { + showErrorMessage("ERROR: Invalid App Name: $value (needs to follow standard pattern {MyProject} or {My Project})") + } + } + + private fun validatePackageName(value: String) { + if (PATTERN_PACKAGE.toRegex().matches(value)) { + packageName = value + } else { + showErrorMessage("ERROR: Invalid Package Name: $value (needs to follow standard pattern {com.example.package})") + } + } + + private fun initializeNewProjectFolder() { + showMessage("=> 🐢 Initializing new project...") + copyFiles(fromPath = rootPath + TEMPLATE_FOLDER_NAME, toPath = projectPath) + // Set gradlew file as executable, because copying files from one folder to another doesn't copy file permissions correctly (= read, write & execute). + File(projectPath + fileSeparator + "gradlew")?.setExecutable(true) + } + + private fun cleanNewProjectFolder() { + executeCommand("sh $projectPath${fileSeparator}gradlew -p $projectPath clean") + executeCommand("sh $projectPath${fileSeparator}gradlew -p $projectPath${fileSeparator}buildSrc clean") + listOf(".idea", ".gradle", "buildSrc$fileSeparator.gradle", ".git").forEach { + File("$projectPath$fileSeparator$it")?.let { targetFile -> + targetFile.deleteRecursively() + } + } + } + + private fun renamePackageNameFolders() { + showMessage("=> 🔎 Renaming the package folders...") + modules.forEach { module -> + val srcPath = projectPath + fileSeparator + module + fileSeparator + "src" + File(srcPath) + .walk() + .maxDepth(2) + .filter { it.isDirectory && it.name == "java" } + .forEach { javaDirectory -> + val oldDirectory = File( + javaDirectory, TEMPLATE_PACKAGE_NAME.replace( + oldValue = DOT_SEPARATOR, + newValue = fileSeparator + ) + ) + val newDirectory = File( + javaDirectory, packageName.replace( + oldValue = DOT_SEPARATOR, + newValue = fileSeparator + ) + ) + + val tempDirectory = File(javaDirectory, "temp_directory") + copyFiles( + fromPath = oldDirectory.absolutePath, + toPath = tempDirectory.absolutePath + ) + oldDirectory.parentFile?.parentFile?.deleteRecursively() + newDirectory.mkdirs() + copyFiles( + fromPath = tempDirectory.absolutePath, + toPath = newDirectory.absolutePath + ) + tempDirectory.deleteRecursively() + } + } + } + + private fun renamePackageNameWithinFiles() { + showMessage("=> 🔎 Renaming package name within files...") + File(projectPath) + ?.walk() + .filter { it.name.endsWith(".kt") || it.name.endsWith(".xml") } + .forEach { filePath -> + rename( + sourcePath = filePath.toString(), + oldValue = TEMPLATE_PACKAGE_NAME, + newValue = packageName + ) + } + } + + private fun renameApplicationClass() { + showMessage("=> 🔎 Renaming application class...") + File(projectPath) + ?.walk() + .filter { it.name == "$TEMPLATE_APPLICATION_CLASS_NAME.kt" || it.name == "AndroidManifest.xml" } + .forEach { file -> + rename( + sourcePath = file.absolutePath, + oldValue = TEMPLATE_APPLICATION_CLASS_NAME, + newValue = applicationClassName + ) + if (file.name == "$TEMPLATE_APPLICATION_CLASS_NAME.kt") { + val newApplicationPath = file.absolutePath.replaceAfterLast( + delimiter = fileSeparator, + replacement = "$applicationClassName.kt" + ) + val newApplicationFile = File(newApplicationPath) + file.renameTo(newApplicationFile) + } + } + } + + private fun buildProjectAndRunTests() { + showMessage("=> 🛠️ Building project...") + executeCommand("sh $projectPath${fileSeparator}gradlew -p /$projectPath assembleDebug") + showMessage("=> 🚓 Running tests...") + executeCommand("sh $projectPath${fileSeparator}gradlew -p /$projectPath testStagingDebugUnitTest") + showMessage("=> 🚀 Done! The project is ready for development") + } + + private fun copyFiles(fromPath: String, toPath: String) { + val targetFolder = File(toPath) + val sourceFolder = File(fromPath) + sourceFolder.copyRecursively(targetFolder, true) { file, exception -> + showMessage(exception?.message ?: "Error copying files") + return@copyRecursively OnErrorAction.TERMINATE + } + } + + private fun executeCommand(command: String) { + val process = Runtime.getRuntime().exec(command) + process.inputStream.reader().forEachLine { println(it) } + val exitValue = process.waitFor() + if (exitValue != 0) { + showErrorMessage("❌ Something went wrong!", exitValue) + } + } + + private fun renameAppName() { + showMessage("=> 🔎 Renaming app name...") + File(projectPath) + ?.walk() + .filter { it.name == "strings.xml" } + .forEach { filePath -> + rename( + sourcePath = filePath.toString(), + oldValue = TEMPLATE_APP_NAME, + newValue = appName + ) + } + } + + private fun rename(sourcePath: String, oldValue: String, newValue: String) { + val sourceFile = File(sourcePath) + var sourceText = sourceFile.readText() + sourceText = sourceText.replace(oldValue, newValue) + sourceFile.writeText(sourceText) + } + + private fun showMessage(message: String) { + println("\n${message}\n") + } + + private fun showErrorMessage(message: String, exitCode: Int = 0) { + println("\n${message}\n") + System.exit(exitCode) + } +} + +NewProject.generate(args)