-
Notifications
You must be signed in to change notification settings - Fork 933
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add module templating to streamline new module creation (#3799)
<!-- Note: This checklist is a reminder of our shared engineering expectations. The items in Bold are required If your PR involves UI changes: 1. Upload screenshots or screencasts that illustrate the changes before / after 2. Add them under the UI changes section (feel free to add more columns if needed) If your PR does not involve UI changes, you can remove the **UI changes** section At a minimum, make sure your changes are tested in API 23 and one of the more recent API levels available. --> Task/Issue URL: https://app.asana.com/0/0/1205678561572065/f ### Description Adds tooling to allow for creation of new modules which come with the recommended structure and gradle dependencies. Supported module types: - Pure Kotlin API modules - Android-aware API modules - impl modules Note: `store` modules have recently became redundant and is therefore not supported through this tooling. ### Steps to test this PR #### Sense-check the usage instructions - [x] Run `./gradlew newModule`, which will only output usage instructions - [x] Verify the instructions make sense #### Add new kotlin API module - [x] Run `./gradlew newModule -Pfeature=brand-new-feature/api` - [x] Verify a new directory is created in the project root called `brand-new-feature`, and inside is `brand-new-feature-api` - [x] Open the new `build.gradle` and verify it's a pure Kotlin module type - [x] Open the app's `build.gradle` and verify it's added the dependency `implementation project(":brand-new-feature-api")` #### Add new kotlin impl module - [x] Run `./gradlew newModule -Pfeature=brand-new-feature/impl` - [x] Verify `brand-new-feature/brand-new-feature-impl` now exists - [x] Open the new `build.gradle` and verify it looks as expected, and has a sensible namespace set - [x] Open the app's `build.gradle` and verify it's added the dependency `implementation project(":brand-new-feature-impl")` #### Switch to Android-API module type - [x] Delete `brand-new-feature-api` module directory, and remove its dependency from: - [x] app's `build.gradle` - [x] `brand-new-feature-impl`'s `build.gradle` - [x] Run `./gradlew newModule -Pfeature=brand-new-feature/apiandroid` - [x] Verify `brand-new-feature/brand-new-feature-api` now exists - [x] Open the new `build.gradle` and verify it's an Android-aware API module #### Will not update an existing module For safety, the tooling will fail if you run it against an existing module - [x] Run `./gradlew newModule -Pfeature=settings/api` and verify it fails due to `settings/settings-api` already existing
- Loading branch information
Showing
17 changed files
with
809 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
# This is the module where the Android Design System resides | ||
# Any changes to the source files of this module require approval from @malmstein or @nalcalag. | ||
/common/common-ui/ @malmstein @nalcalag | ||
/saved-sites/ @malmstein | ||
/saved-sites/ @malmstein | ||
/example-feature/ @cdrussell |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
plugins { | ||
id 'org.jetbrains.kotlin.jvm' version "1.9.20" | ||
} | ||
|
||
repositories { | ||
mavenCentral() | ||
} | ||
|
||
dependencies { | ||
testImplementation platform('org.junit:junit-bom:5.9.1') | ||
testImplementation 'org.junit.jupiter:junit-jupiter' | ||
} | ||
|
||
test { | ||
useJUnitPlatform() | ||
} |
110 changes: 110 additions & 0 deletions
110
buildSrc/src/main/java/com/duckduckgo/gradle/BuildGradleModifier.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
/* | ||
* Copyright (c) 2023 DuckDuckGo | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.duckduckgo.gradle | ||
|
||
import org.gradle.api.GradleException | ||
import java.io.File | ||
|
||
class BuildGradleModifier(private val gradleFile: File) { | ||
|
||
fun removeDependency(placeholder: String) { | ||
val lines = gradleFile.readText().lines() | ||
val updatedLines = mutableListOf<String>() | ||
|
||
for (line in lines) { | ||
if (!line.contains(placeholder)) { | ||
updatedLines.add(line) | ||
} | ||
} | ||
|
||
val newFileOutput = updatedLines.joinToString(System.lineSeparator()) | ||
gradleFile.writeText(newFileOutput) | ||
} | ||
|
||
fun insertDependencies( | ||
featureName: String, | ||
dependencies: List<String> | ||
) { | ||
if(dependencies.isEmpty()) return | ||
|
||
val searchString = "dependencies {" | ||
val linesToAdd = dependencies.map { """implementation project(":${featureName}-${it}")""" } | ||
|
||
val lines = gradleFile.readText().lines() | ||
val updatedLines = mutableListOf<String>() | ||
var found = false | ||
|
||
for (line in lines) { | ||
updatedLines.add(line) | ||
|
||
if (line.contains(searchString)) { | ||
linesToAdd.forEach { | ||
if (!lines.contains("\t$it")) { | ||
println("Inserting [$it] into [${gradleFile.parentFile.name}/${gradleFile.name}]") | ||
updatedLines.add("\t$it") | ||
} | ||
} | ||
found = true | ||
} | ||
} | ||
|
||
if (!found) { | ||
throw GradleException("Could not insert dependencies into build.gradle because could not locate correct place to insert them.") | ||
} | ||
|
||
val newFileOutput = updatedLines.joinToString(System.lineSeparator()) | ||
gradleFile.writeText(newFileOutput) | ||
} | ||
|
||
fun renameModuleNamespace( | ||
featureName: String, | ||
moduleType: String, | ||
) { | ||
val lines = gradleFile.readText().lines() | ||
val updatedLines = mutableListOf<String>() | ||
var found = false | ||
val placeholder = moduleNamespacePlaceholder(moduleType) | ||
val newNamespace = buildNewNamespace(featureName, moduleType) | ||
|
||
for (line in lines) { | ||
if (line.contains(placeholder)) { | ||
updatedLines.add(newNamespace) | ||
found = true | ||
} else { | ||
updatedLines.add(line) | ||
} | ||
} | ||
|
||
if (!found) { | ||
throw GradleException("Could not update namespace for [$featureName/$moduleType] because namespace placeholder not found in new module's build.gradle") | ||
} | ||
|
||
val newFileOutput = updatedLines.joinToString(System.lineSeparator()) | ||
gradleFile.writeText(newFileOutput) | ||
} | ||
|
||
private fun buildNewNamespace( | ||
featureName: String, | ||
moduleType: String | ||
): String { | ||
return """ namespace "com.duckduckgo.$featureName.${moduleType}"""" | ||
} | ||
|
||
private fun moduleNamespacePlaceholder(moduleType: String): String { | ||
return """namespace "com.duckduckgo.examplefeature.${moduleType}"""" | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
buildSrc/src/main/java/com/duckduckgo/gradle/InputExtractor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/* | ||
* Copyright (c) 2023 DuckDuckGo | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.duckduckgo.gradle | ||
|
||
import org.gradle.api.GradleException | ||
|
||
class InputExtractor { | ||
|
||
fun extractFeatureNameAndTypes(input: String): Pair<String, ModuleType> { | ||
validateFeatureName(input) | ||
val (featureName, moduleType) = input.split("/") | ||
validateModuleType(moduleType) | ||
return Pair(featureName, ModuleType.moduleTypeFromInput(moduleType)).also { | ||
println("""Feature is [$featureName] and module type is [${moduleType}]""") | ||
} | ||
} | ||
|
||
private fun validateFeatureName(featureName: String) { | ||
if (featureName.isEmpty()) throw GradleException(ERROR_MESSAGE_EMPTY_NAME) | ||
if (!featureName.matches(ACCEPTABLE_CHARACTERS_REGEX)) throw GradleException(ERROR_MESSAGE_INVALID_CHARS) | ||
if (featureName.count { it == '/' } != 1) throw GradleException(ERROR_MESSAGE_UNEXPECTED_NUMBER_OF_FORWARD_SLASHES) | ||
if (featureName.startsWith("/")) throw GradleException(ERROR_MESSAGE_STARTS_WITH_FORWARD_SLASH) | ||
} | ||
|
||
private fun validateModuleType(moduleType: String) { | ||
if (!moduleType.matches(VALID_MODULE_TYPES_REGEX)) { | ||
throw GradleException("Invalid module type [$moduleType]. Must be one of [ ${ModuleType.validInputTypes().joinToString(" | ")} ]") | ||
} | ||
} | ||
|
||
companion object { | ||
private val VALID_MODULE_TYPES_REGEX = "^(${ModuleType.validInputTypes().joinToString("|")})$".toRegex() | ||
private val ACCEPTABLE_CHARACTERS_REGEX = "^[a-z0-9-/]*$".toRegex() | ||
|
||
private const val ERROR_MESSAGE_EMPTY_NAME = "Feature name cannot be empty" | ||
private const val ERROR_MESSAGE_UNEXPECTED_NUMBER_OF_FORWARD_SLASHES = "Feature name must contain exactly one forward slash" | ||
private const val ERROR_MESSAGE_STARTS_WITH_FORWARD_SLASH = "Feature name cannot start with a forward slash" | ||
private const val ERROR_MESSAGE_INVALID_CHARS = | ||
"Feature name can only contain lowercase letters, numbers, dashes and one forward slash" | ||
} | ||
} |
68 changes: 68 additions & 0 deletions
68
buildSrc/src/main/java/com/duckduckgo/gradle/IntermoduleDependencyManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/* | ||
* Copyright (c) 2023 DuckDuckGo | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.duckduckgo.gradle | ||
|
||
import com.duckduckgo.gradle.ModuleType.ApiPureKotlin | ||
import com.duckduckgo.gradle.ModuleType.Companion.destinationDirectorySuffix | ||
import com.duckduckgo.gradle.ModuleType.Impl | ||
import java.io.File | ||
|
||
class IntermoduleDependencyManager { | ||
|
||
fun wireUpIntermoduleDependencies(newFeatureDestination: File) { | ||
val apiModule = File(newFeatureDestination, "${newFeatureDestination.name}-${ApiPureKotlin.destinationDirectorySuffix()}") | ||
val implModule = File(newFeatureDestination, "${newFeatureDestination.name}-${Impl.destinationDirectorySuffix()}") | ||
|
||
wireUpImplModule(apiModule, implModule, newFeatureDestination) | ||
} | ||
|
||
private fun wireUpImplModule( | ||
apiModule: File, | ||
implModule: File, | ||
newFeatureDestination: File | ||
) { | ||
if (implModule.exists()) { | ||
println("Wiring up module: ${implModule.name}") | ||
val gradleModifier = BuildGradleModifier(File(implModule, BUILD_GRADLE)) | ||
|
||
// delete placeholder | ||
gradleModifier.removeDependency(PLACEHOLDER_API_DEPENDENCY) | ||
|
||
// conditionally insert dependencies | ||
val modules = mutableListOf<String>() | ||
if (apiModule.exists()) { | ||
modules.add(ApiPureKotlin.destinationDirectorySuffix()) | ||
} | ||
gradleModifier.insertDependencies(newFeatureDestination.name, modules) | ||
} | ||
} | ||
|
||
fun wireUpAppModule( | ||
featureName: String, | ||
moduleType: ModuleType, | ||
buildGradleFile: File, | ||
) { | ||
println("Wiring up app module to include feature: name=[$featureName], type=[${moduleType.javaClass.simpleName}]") | ||
val gradleModifier = BuildGradleModifier(buildGradleFile) | ||
gradleModifier.insertDependencies(featureName, listOf(moduleType.destinationDirectorySuffix())) | ||
} | ||
|
||
companion object { | ||
private const val PLACEHOLDER_API_DEPENDENCY = "implementation project(':example-feature-api')" | ||
private const val BUILD_GRADLE = "build.gradle" | ||
} | ||
} |
Oops, something went wrong.