├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .run ├── Run IDE for UI Tests.run.xml ├── Run IDE with Plugin.run.xml ├── Run Plugin Tests.run.xml ├── Run Plugin Verification.run.xml └── Run Qodana.run.xml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── icon.svg └── marketplace.png ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml ├── spotless.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── com │ │ └── joetr │ │ └── modulemaker │ │ ├── Constants.kt │ │ ├── MessageDialogWrapper.kt │ │ ├── ModuleMakerAction.kt │ │ ├── ModuleMakerDialogWrapper.kt │ │ ├── MultiplatformSourceSets.kt │ │ ├── Notifications.kt │ │ ├── PreviewDialogWrapper.kt │ │ ├── SettingsDialogWrapper.kt │ │ ├── data │ │ ├── File.kt │ │ ├── ToProjectFile.kt │ │ └── analytics │ │ │ ├── ModuleCreationAnalytics.kt │ │ │ └── ModuleCreationErrorAnalytics.kt │ │ ├── file │ │ └── FileWriter.kt │ │ ├── persistence │ │ ├── PreferenceService.kt │ │ └── PreferenceServiceImpl.kt │ │ ├── template │ │ ├── AndroidModuleKtsTemplate.kt │ │ ├── AndroidModuleTemplate.kt │ │ ├── GitIgnoreTemplate.kt │ │ ├── KotlinModuleKtsTemplate.kt │ │ ├── KotlinModuleTemplate.kt │ │ ├── ModuleReadMeTemplate.kt │ │ ├── MultiplatformKtsTemplate.kt │ │ ├── TemplateVariable.kt │ │ └── TemplateWriter.kt │ │ └── ui │ │ ├── LabelledCheckbox.kt │ │ ├── file │ │ ├── FileTree.kt │ │ └── FileTreeView.kt │ │ └── theme │ │ ├── Color.kt │ │ ├── Shape.kt │ │ ├── Type.kt │ │ ├── WidgetTheme.kt │ │ └── intellij │ │ ├── SwingColor.kt │ │ └── ThemeChangeListener.kt └── resources │ └── META-INF │ ├── plugin.xml │ └── pluginIcon.svg └── test └── kotlin └── com └── joetr └── modulemaker ├── AndroidModuleMakerTest.kt ├── EnhancedModuleMakerTest.kt ├── KotlinModuleMakerTest.kt ├── MultiplatformModuleMakerTest.kt ├── TestConstants.kt ├── TestUtilities.kt └── settings ├── DefaultTemplateSettingsGradle.kt ├── NowInAndroidSettingsGradleKts.kt └── TiviSettingsGradleKts.kt /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-java@v3 18 | with: 19 | distribution: temurin 20 | java-version: 17 21 | 22 | - name: Setup Gradle 23 | uses: gradle/gradle-build-action@v2 24 | 25 | - name: Execute Gradle build 26 | run: ./gradlew build --stacktrace -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'release/[0-9]+.[0-9]+.[0-9]+' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build-and-release: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-java@v3 19 | with: 20 | distribution: temurin 21 | java-version: 17 22 | 23 | - name: Setup Gradle 24 | uses: gradle/gradle-build-action@v2 25 | 26 | - name: Execute Gradle build 27 | run: ./gradlew buildPlugin --stacktrace 28 | 29 | - name: Upload and Release 30 | uses: fnkr/github-action-ghr@v1 31 | env: 32 | GHR_PATH: build/distributions/ 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Log/OS Files 9 | *.log 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.apk 16 | output.json 17 | 18 | # IntelliJ 19 | *.iml 20 | .idea/ 21 | misc.xml 22 | deploymentTargetDropDown.xml 23 | render.experimental.xml 24 | 25 | # Keystore files 26 | *.jks 27 | *.keystore 28 | 29 | # Google Services (e.g. APIs or Firebase) 30 | google-services.json 31 | 32 | # Android Profiling 33 | *.hprof 34 | 35 | .DS_Store 36 | 37 | .intellijPlatform 38 | -------------------------------------------------------------------------------- /.run/Run IDE for UI Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 15 | 17 | true 18 | true 19 | false 20 | 21 | 22 | -------------------------------------------------------------------------------- /.run/Run IDE with Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.run/Run Plugin Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.run/Run Plugin Verification.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 25 | 26 | -------------------------------------------------------------------------------- /.run/Run Qodana.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 16 | 19 | 21 | true 22 | true 23 | false 24 | 25 | 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Module Maker Changelog 2 | 3 | ## [1.1.1] 4 | - Platform updates 5 | 6 | ## [1.1.0] 7 | - Support Multiplatform modules 8 | 9 | ## [1.0.26] 10 | - Platform updates 11 | 12 | ## [1.0.25] 13 | - Update window sizes 14 | 15 | ## [1.0.24] 16 | - Update Platform Version 17 | 18 | ## [1.0.23] 19 | - Add file preview 20 | 21 | ## [1.0.22] 22 | - Don't specify pluginUntilBuild. This plugin will work forever ;) 23 | 24 | ## [1.0.21] 25 | - Update supported platform versions 26 | 27 | ## [1.0.20] 28 | - Accept starting file location when starting Module Maker 29 | 30 | ## [1.0.19] 31 | - Dependency updates 32 | 33 | ## [1.0.18] 34 | - Attempt to fix crash on startup (again) 35 | 36 | ## [1.0.17] 37 | - Attempt to fix crash on startup 38 | 39 | ## [1.0.16] 40 | - Finished Compose UI migration (all UI is now in Compose!) 41 | - Added basic telemetry. There is no project or user specific data collected, just general module settings used when creating a module. 42 | 43 | ## [1.0.15] 44 | - Compose UI migration 45 | 46 | ## [1.0.14] 47 | - Support new intellij platform versions 48 | - Update to Kotlin 1.9 49 | - Better support for smaller screens 50 | 51 | ## [1.0.13] 52 | - Support for multi-app projects with multiple settings.gradle(.kts) files 53 | - Support a custom "include" keyword to be used to include modules in settings.gradle(.kts) files 54 | 55 | ## [1.0.12] 56 | - Performance improvement for large projects 57 | 58 | ## [1.0.11] 59 | - Added better support around multiple include keywords in settings.gradle.kts file 60 | 61 | ## [1.0.10] 62 | - Added support for custom module names for api / glue / impl modules 63 | 64 | ## [1.0.9] 65 | - Added support for file paths in settings.gradle.kts file 66 | 67 | ## [1.0.8] 68 | - Fixed issue when multiple projects were open 69 | 70 | ## [1.0.7] 71 | - Add option to add README.md when generating a module 72 | - Add option to add .gitignore to module creation 73 | - Added new section in settings to specify custom .gitignore template 74 | - Added settings under 'General' tab to set defaults for most module configurations 75 | 76 | ## [1.0.6] 77 | - Add import / export in settings 78 | 79 | ## [1.0.5] 80 | - Internal improvements 81 | 82 | ## [1.0.4] 83 | - Add support for variables in custom templates 84 | 85 | ## [1.0.3] 86 | - Auto sync project after module creation 87 | 88 | ## [1.0.2] 89 | - Added question mark next to 3 module checkbox 90 | 91 | ## [1.0.1] 92 | - Compatibility fixes 93 | 94 | ## [1.0.0] 95 | - Initial release 96 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at {{ email }}. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Transcriptase 2 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 3 | 4 | - Reporting a bug 5 | - Discussing the current state of the code 6 | - Submitting a fix 7 | - Proposing new features 8 | - Becoming a maintainer 9 | 10 | ## We Develop with Github 11 | We use github to host code, to track issues and feature requests, as well as accept pull requests. 12 | 13 | ## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests 14 | Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests: 15 | 16 | 1. Fork the repo and create your branch from `master`. 17 | 2. If you've added code that should be tested, add tests. 18 | 3. If you've changed APIs, update the documentation. 19 | 4. Ensure the test suite passes. 20 | 5. Make sure your code lints. 21 | 6. Issue that pull request! 22 | 23 | ## Any contributions you make will be under the MIT Software License 24 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 25 | 26 | ## Report bugs using Github's [issues](https://github.com/briandk/transcriptase-atom/issues) 27 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy! 28 | 29 | ## Write bug reports with detail, background, and sample code 30 | [This is an example](http://stackoverflow.com/q/12488905/180626) of a bug report I wrote, and I think it's not a bad model. Here's [another example from Craig Hockenberry](http://www.openradar.me/11905408), an app developer whom I greatly respect. 31 | 32 | **Great Bug Reports** tend to have: 33 | 34 | - A quick summary and/or background 35 | - Steps to reproduce 36 | - Be specific! 37 | - Give sample code if you can. [My stackoverflow question](http://stackoverflow.com/q/12488905/180626) includes sample code that *anyone* with a base R setup can run to reproduce what I was seeing 38 | - What you expected would happen 39 | - What actually happens 40 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 41 | 42 | People *love* thorough bug reports. I'm not even kidding. 43 | 44 | ## License 45 | By contributing, you agree that your contributions will be licensed under its MIT License. 46 | 47 | ## References 48 | This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md) 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 j-roskopf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Module Maker

4 |
5 | 6 |

7 | License 8 | Android Weekly 9 | Kotlin Weekly 10 | Release Workflow 11 | 12 | 13 | 14 |


15 | 16 | 17 | This is a plugin that allows one to create modules without having to copy / paste / modify existing modules. 18 | 19 | Creating both single modules and enhanced modules (representing the 3 module system outline [here](https://www.droidcon.com/2019/11/15/android-at-scale-square/)) 20 | 21 | Additional features include: 22 | 23 | 1. Specifying gradle template for modules to align with your project specific defaults. 24 | 1. Allows for custom variables to be replaced with generated values 25 | 2. Aligning the gradle files to follow the module name 26 | 3. Generating both .gradle and .gradle.kts build files for a given module 27 | 28 | 29 | # Demo 30 | 31 | https://www.youtube.com/watch?v=ZtXCxBuiQNk 32 | 33 | ## Building 34 | 35 | Creating a release tag that follows `release/x.x.x` will create a Github release with the relevant artifacts. 36 | 37 | ## How To Use 38 | 39 | - From under the `Tools` menu 40 | 41 | Tools > Module Maker 42 | 43 | ## Installation 44 | 45 |
46 | 47 | - Using IDE built-in plugin system: 48 | 49 | Settings/Preferences > Plugins > Marketplace > Search for "Module Maker" > 50 | Install Plugin 51 | 52 | - Manually: 53 | 54 | Download the [latest release](https://github.com/j-roskopf/ModuleMakerPlugin/releases/latest) and install it manually using 55 | Settings/Preferences > Plugins > ⚙️ > Install plugin from disk... 56 | -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/marketplace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-roskopf/ModuleMakerPlugin/be7483caa297610fa4874b1931c28f9431edbe7d/assets/marketplace.png -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.changelog.Changelog 2 | import org.jetbrains.changelog.markdownToHTML 3 | 4 | fun properties(key: String) = providers.gradleProperty(key) 5 | fun environment(key: String) = providers.environmentVariable(key) 6 | 7 | plugins { 8 | id("java") // Java support 9 | alias(libs.plugins.kotlin) // Kotlin support 10 | alias(libs.plugins.gradleIntelliJPlugin) // Gradle IntelliJ Plugin 11 | alias(libs.plugins.changelog) // Gradle Changelog Plugin 12 | kotlin("plugin.serialization") version libs.versions.kotlin.get() 13 | id("org.jetbrains.compose") 14 | alias(libs.plugins.spotless) 15 | alias(libs.plugins.compose) 16 | } 17 | 18 | group = properties("pluginGroup").get() 19 | version = properties("pluginVersion").get() 20 | 21 | buildscript { 22 | repositories { 23 | mavenCentral() 24 | google() 25 | maven { url = uri("https://plugins.gradle.org/m2/") } 26 | maven { 27 | url = uri("https://www.jetbrains.com/intellij-repository/releases") 28 | } 29 | } 30 | } 31 | 32 | // Configure project's dependencies 33 | repositories { 34 | mavenCentral() 35 | google() 36 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 37 | maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } 38 | intellijPlatform { 39 | defaultRepositories() 40 | } 41 | } 42 | 43 | apply( 44 | from = "gradle/spotless.gradle" 45 | ) 46 | 47 | dependencies { 48 | implementation(libs.freemarker) 49 | implementation(libs.serialization) 50 | implementation(compose.desktop.currentOs) 51 | implementation(compose.materialIconsExtended) 52 | implementation(libs.segment) 53 | 54 | val version = "0.8.18" 55 | val macTarget = "macos-arm64" 56 | val windowsTarget = "windows-x64" 57 | val linuxTarget = "linux-x64" 58 | 59 | implementation("org.jetbrains.skiko:skiko-awt-runtime-$macTarget:$version") 60 | implementation("org.jetbrains.skiko:skiko-awt-runtime-$windowsTarget:$version") 61 | implementation("org.jetbrains.skiko:skiko-awt-runtime-$linuxTarget:$version") 62 | 63 | testImplementation(libs.junit) 64 | 65 | // IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html 66 | intellijPlatform { 67 | javaCompiler("243.26053.29") // https://github.com/JetBrains/intellij-platform-gradle-plugin/issues/1894 68 | create(properties("platformType").get(), properties("platformVersion").get()) 69 | 70 | // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. 71 | bundledPlugins(properties("platformBundledPlugins").map { it.split(',') }) 72 | 73 | // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file for plugin from JetBrains Marketplace. 74 | plugins(properties("platformPlugins").map { it.split(',') }) 75 | 76 | // instrumentationTools() 77 | pluginVerifier() 78 | zipSigner() 79 | } 80 | } 81 | 82 | kotlin { 83 | jvmToolchain(libs.versions.jdk.get().toInt()) 84 | } 85 | 86 | // Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin 87 | changelog { 88 | groups.empty() 89 | repositoryUrl = properties("pluginRepositoryUrl") 90 | } 91 | 92 | tasks { 93 | wrapper { 94 | gradleVersion = properties("gradleVersion").get() 95 | } 96 | 97 | patchPluginXml { 98 | version = properties("pluginVersion").get() 99 | sinceBuild = properties("pluginSinceBuild").get() 100 | // untilBuild = properties("pluginUntilBuild").get() 101 | 102 | // Extract the section from README.md and provide for the plugin's manifest 103 | pluginDescription = providers.fileContents(layout.projectDirectory.file("README.md")).asText.map { 104 | val start = "" 105 | val end = "" 106 | 107 | with(it.lines()) { 108 | if (!containsAll(listOf(start, end))) { 109 | throw GradleException("Plugin description section not found in README.md:\n$start ... $end") 110 | } 111 | subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML) 112 | } 113 | } 114 | 115 | val changelog = project.changelog // local variable for configuration cache compatibility 116 | // Get the latest available change notes from the changelog file 117 | changeNotes = properties("pluginVersion").map { pluginVersion -> 118 | with(changelog) { 119 | renderItem( 120 | (getOrNull(pluginVersion) ?: getUnreleased()) 121 | .withHeader(false) 122 | .withEmptySections(false), 123 | Changelog.OutputType.HTML 124 | ) 125 | } 126 | } 127 | } 128 | 129 | signPlugin { 130 | certificateChain = environment("CERTIFICATE_CHAIN") 131 | privateKey = environment("PRIVATE_KEY") 132 | password = environment("PRIVATE_KEY_PASSWORD") 133 | } 134 | } 135 | 136 | intellijPlatformTesting { 137 | runIde { 138 | register("runIdeForUiTests") { 139 | task { 140 | jvmArgumentProviders += CommandLineArgumentProvider { 141 | listOf( 142 | "-Drobot-server.port=8082", 143 | "-Dide.mac.message.dialogs.as.sheets=false", 144 | "-Djb.privacy.policy.text=", 145 | "-Djb.consents.confirmation.enabled=false" 146 | ) 147 | } 148 | } 149 | 150 | plugins { 151 | robotServerPlugin() 152 | } 153 | } 154 | } 155 | } 156 | 157 | // Configure IntelliJ Platform Gradle Plugin - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-extension.html 158 | intellijPlatform { 159 | pluginConfiguration { 160 | version = properties("pluginVersion").get() 161 | 162 | // Extract the section from README.md and provide for the plugin's manifest 163 | description = providers.fileContents(layout.projectDirectory.file("README.md")).asText.map { 164 | val start = "" 165 | val end = "" 166 | 167 | with(it.lines()) { 168 | if (!containsAll(listOf(start, end))) { 169 | throw GradleException("Plugin description section not found in README.md:\n$start ... $end") 170 | } 171 | subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML) 172 | } 173 | } 174 | 175 | val changelog = project.changelog // local variable for configuration cache compatibility 176 | // Get the latest available change notes from the changelog file 177 | changeNotes = properties("pluginVersion").map { pluginVersion -> 178 | with(changelog) { 179 | renderItem( 180 | (getOrNull(pluginVersion) ?: getUnreleased()) 181 | .withHeader(false) 182 | .withEmptySections(false), 183 | Changelog.OutputType.HTML 184 | ) 185 | } 186 | } 187 | 188 | ideaVersion { 189 | sinceBuild = properties("pluginSinceBuild").get() 190 | // untilBuild = properties("pluginUntilBuild").get() 191 | } 192 | } 193 | 194 | signing { 195 | certificateChain = providers.environmentVariable("CERTIFICATE_CHAIN") 196 | privateKey = providers.environmentVariable("PRIVATE_KEY") 197 | password = providers.environmentVariable("PRIVATE_KEY_PASSWORD") 198 | } 199 | 200 | publishing { 201 | token = providers.environmentVariable("PUBLISH_TOKEN") 202 | // The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 203 | // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: 204 | // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel 205 | channels = properties("pluginVersion") 206 | .map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) } 207 | } 208 | 209 | pluginVerification { 210 | ides { 211 | recommended() 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 2 | 3 | pluginGroup = com.joetr.modulemaker 4 | pluginName = ModuleMaker 5 | pluginRepositoryUrl = https://github.com/j-roskopf/ModuleMakerPlugin 6 | # SemVer format -> https://semver.org 7 | pluginVersion = 1.1.1 8 | 9 | # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 10 | pluginSinceBuild = 222 11 | # pluginUntilBuild = 12 | 13 | # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension 14 | platformType = AI 15 | # AS version and patch at the end 16 | platformVersion = 2024.3.1.1 17 | 18 | # Example: platformBundledPlugins = com.intellij.java 19 | platformBundledPlugins = com.intellij.java 20 | 21 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 22 | # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 23 | platformPlugins = 24 | 25 | # Gradle Releases -> https://github.com/gradle/gradle/releases 26 | # update gradle-wrapper.properties and run ./gradlew wrapper 27 | gradleVersion = 8.13 28 | 29 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib 30 | kotlin.stdlib.default.dependency = false 31 | 32 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 33 | # TODO - Enable - seems to fail on CI 34 | org.gradle.configuration-cache = false 35 | 36 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 37 | org.gradle.caching = true 38 | 39 | # Enable Gradle Kotlin DSL Lazy Property Assignment -> https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotdsl:assignment 40 | systemProp.org.gradle.unsafe.kotlin.assignment = true 41 | 42 | compose.version=1.7.3 43 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | freemarker = "2.3.30" 3 | serialization = "1.5.1" 4 | jdk = "17" 5 | kotlin = "2.1.20" 6 | changelog = "2.0.0" 7 | gradleIntelliJPlugin = "2.4.0" 8 | spotless = "6.8.0" 9 | segment = "1.13.2" 10 | junit = "4.13.2" 11 | 12 | [libraries] 13 | freemarker = { group = "org.freemarker", name = "freemarker", version.ref = "freemarker" } 14 | serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } 15 | segment = { group = "com.segment.analytics.kotlin", name = "core", version.ref = "segment" } 16 | junit = { group = "junit", name = "junit", version.ref = "junit" } 17 | 18 | [plugins] 19 | changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } 20 | compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 21 | gradleIntelliJPlugin = { id = "org.jetbrains.intellij.platform", version.ref = "gradleIntelliJPlugin" } 22 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 23 | spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } 24 | -------------------------------------------------------------------------------- /gradle/spotless.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.diffplug.spotless" 2 | 3 | spotless { 4 | java { 5 | target '**/*.java' 6 | googleJavaFormat().aosp() 7 | removeUnusedImports() 8 | trimTrailingWhitespace() 9 | indentWithSpaces() 10 | endWithNewline() 11 | } 12 | 13 | kotlinGradle { 14 | ktlint("0.46.0") 15 | trimTrailingWhitespace() 16 | endWithNewline() 17 | } 18 | 19 | kotlin { 20 | target '**/*.kt' 21 | ktlint("0.46.0") 22 | trimTrailingWhitespace() 23 | indentWithSpaces() 24 | endWithNewline() 25 | } 26 | 27 | format 'misc', { 28 | target '**/*.gradle', '**/*.md', '**/.gitignore' 29 | indentWithSpaces() 30 | trimTrailingWhitespace() 31 | endWithNewline() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-roskopf/ModuleMakerPlugin/be7483caa297610fa4874b1931c28f9431edbe7d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Module Maker" 2 | 3 | pluginManagement { 4 | plugins { 5 | id("org.jetbrains.compose").version(extra["compose.version"] as String) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker 2 | 3 | import freemarker.template.Configuration 4 | import freemarker.template.Version 5 | 6 | const val DEFAULT_PADDING = 4 7 | const val EXTRA_PADDING = 8 8 | const val SCROLLBAR_WIDTH = 40 9 | 10 | const val DEFAULT_EXIT_CODE = 2 11 | 12 | val FREEMARKER_VERSION: Version = Configuration.VERSION_2_3_30 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/MessageDialogWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker 2 | 3 | import com.intellij.openapi.ui.DialogWrapper 4 | import org.jetbrains.annotations.Nullable 5 | import java.awt.BorderLayout 6 | import java.awt.Dimension 7 | import javax.swing.Action 8 | import javax.swing.JComponent 9 | import javax.swing.JPanel 10 | import javax.swing.JTextArea 11 | 12 | private const val WINDOW_WIDTH = 100 13 | private const val WINDOW_HEIGHT = 100 14 | 15 | class MessageDialogWrapper(private val message: String) : DialogWrapper(true) { 16 | 17 | init { 18 | init() 19 | } 20 | 21 | @Nullable 22 | override fun createCenterPanel(): JComponent { 23 | val dialogPanel = JPanel(BorderLayout()) 24 | dialogPanel.preferredSize = Dimension(WINDOW_WIDTH, WINDOW_HEIGHT) 25 | 26 | val label = JTextArea(message) 27 | label.isEditable = false 28 | dialogPanel.add(label, BorderLayout.CENTER) 29 | 30 | return dialogPanel 31 | } 32 | 33 | override fun createActions(): Array { 34 | return arrayOf( 35 | DialogWrapperExitAction( 36 | "Okay", 37 | 2 38 | ) 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/ModuleMakerAction.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker 2 | 3 | import com.intellij.openapi.actionSystem.AnAction 4 | import com.intellij.openapi.actionSystem.AnActionEvent 5 | import com.intellij.openapi.actionSystem.CommonDataKeys 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.openapi.vfs.VirtualFile 8 | 9 | class ModuleMakerAction : AnAction() { 10 | override fun actionPerformed(event: AnActionEvent) { 11 | val project: Project = event.project ?: return 12 | 13 | val startingLocation: VirtualFile? = event.getData(CommonDataKeys.VIRTUAL_FILE) 14 | 15 | // we only want to use a starting location if it's coming from a directory 16 | val shouldUseStartingLocation = startingLocation != null && startingLocation.isDirectory 17 | 18 | ModuleMakerDialogWrapper( 19 | project = project, 20 | startingLocation = if (shouldUseStartingLocation) startingLocation else null 21 | ).show() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/MultiplatformSourceSets.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyrightest (c) 2025 Joseph Roskopf 3 | */ 4 | 5 | package com.joetr.modulemaker 6 | 7 | val kotlinMultiplatformSourceSets = listOf( 8 | // Main Source Sets 9 | "commonMain", 10 | "androidMain", 11 | "jvmMain", 12 | "nativeMain", 13 | "iosMain", 14 | "jsMain", 15 | "wasmJsMain", 16 | "iosArm64Main", 17 | "iosX64Main", 18 | "iosSimulatorArm64Main", 19 | "macosMain", 20 | "macosX64Main", 21 | "macosArm64Main", 22 | "linuxMain", 23 | "linuxX64Main", 24 | "linuxArm64Main", 25 | "mingwMain", 26 | "mingwX64Main", 27 | "tvosMain", 28 | "tvosArm64Main", 29 | "tvosX64Main", 30 | "tvosSimulatorArm64Main", 31 | "watchosMain", 32 | "watchosArm32Main", 33 | "watchosArm64Main", 34 | "watchosX64Main", 35 | "watchosSimulatorArm64Main" 36 | ) 37 | 38 | val kotlinMultiplatformTestSourceSets = listOf( 39 | "commonTest", 40 | "androidTest", 41 | "jvmTest", 42 | "nativeTest", 43 | "iosTest", 44 | "jsTest", 45 | "wasmTest", 46 | "iosArm64Test", 47 | "iosX64Test", 48 | "iosSimulatorArm64Test", 49 | "macosTest", 50 | "macosX64Test", 51 | "macosArm64Test", 52 | "linuxTest", 53 | "linuxX64Test", 54 | "linuxArm64Test", 55 | "mingwTest", 56 | "mingwX64Test", 57 | "tvosTest", 58 | "tvosArm64Test", 59 | "tvosX64Test", 60 | "tvosSimulatorArm64Test", 61 | "watchosTest", 62 | "watchosArm32Test", 63 | "watchosArm64Test", 64 | "watchosX64Test", 65 | "watchosSimulatorArm64Test" 66 | ) 67 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/Notifications.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker 2 | 3 | import com.intellij.notification.Notification 4 | import com.intellij.notification.NotificationType 5 | import com.intellij.openapi.project.Project 6 | 7 | object Notifications { 8 | fun showExportError(project: Project) { 9 | val notification = Notification( 10 | "ModuleMaker", 11 | "Error", 12 | "An error occurred while exporting your settings", 13 | NotificationType.ERROR 14 | ) 15 | 16 | notification.notify(project) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/PreviewDialogWrapper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyrightest (c) 2024 Joseph Roskopf 3 | */ 4 | 5 | package com.joetr.modulemaker 6 | 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.material.Surface 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.awt.ComposePanel 15 | import androidx.compose.ui.unit.dp 16 | import com.intellij.openapi.ui.DialogWrapper 17 | import com.intellij.openapi.util.io.FileUtilRt 18 | import com.joetr.modulemaker.data.toProjectFile 19 | import com.joetr.modulemaker.ui.file.FileTree 20 | import com.joetr.modulemaker.ui.file.FileTreeView 21 | import com.joetr.modulemaker.ui.theme.WidgetTheme 22 | import java.io.File 23 | import javax.swing.Action 24 | import javax.swing.JComponent 25 | 26 | private const val WINDOW_WIDTH = 400 27 | private const val WINDOW_HEIGHT = 600 28 | 29 | class PreviewDialogWrapper(val filesToBeCreated: List, val root: String) : DialogWrapper(true) { 30 | 31 | private var tempRoot: File 32 | 33 | init { 34 | title = "Preview" 35 | init() 36 | 37 | tempRoot = FileUtilRt.createTempDirectory(root, null, true) 38 | 39 | createFileStructure( 40 | tempRoot, 41 | filesToBeCreated.map { 42 | val pathToRoot = tempRoot.absolutePath 43 | val split = it.absolutePath.split(root) 44 | 45 | // splice together the files to have a root of our temp folder 46 | File(pathToRoot, split.drop(1).joinToString(separator = "")) 47 | } 48 | ) 49 | } 50 | 51 | override fun dispose() { 52 | super.dispose() 53 | tempRoot.parentFile.deleteRecursively() 54 | } 55 | 56 | override fun createCenterPanel(): JComponent { 57 | return ComposePanel().apply { 58 | setBounds(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT) 59 | setContent { 60 | WidgetTheme { 61 | Surface { 62 | FileTreeJPanel( 63 | modifier = Modifier.height(WINDOW_HEIGHT.dp).width(WINDOW_WIDTH.dp) 64 | ) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | @Composable 72 | private fun FileTreeJPanel( 73 | modifier: Modifier = Modifier 74 | ) { 75 | val height = remember { mutableStateOf(WINDOW_HEIGHT) } 76 | 77 | FileTreeView( 78 | modifier = modifier, 79 | model = FileTree(root = tempRoot.toProjectFile()), 80 | height = height.value.dp, 81 | onClick = { } 82 | ) 83 | } 84 | 85 | private fun List.root(): File { 86 | return this.minBy { file -> 87 | file.absolutePath.count { it.toString() == File.separator } 88 | } 89 | } 90 | 91 | private fun createFileStructure(root: File, structure: List) { 92 | root.mkdirs() 93 | structure.forEach { 94 | it.mkdirs() 95 | if (it.isDirectory.not() && it.extension.isEmpty().not()) { 96 | it.writeText("") 97 | } 98 | } 99 | } 100 | 101 | override fun createActions(): Array { 102 | return arrayOf( 103 | DialogWrapperExitAction( 104 | "Okay", 105 | 2 106 | ) 107 | ) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/data/File.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.data 2 | 3 | interface File { 4 | val name: String 5 | val absolutePath: String 6 | val isDirectory: Boolean 7 | val children: List 8 | val hasChildren: Boolean 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/data/ToProjectFile.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.data 2 | 3 | fun java.io.File.toProjectFile(): File = object : File { 4 | override val name: String 5 | get() = this@toProjectFile.name 6 | 7 | override val absolutePath: String 8 | get() = this@toProjectFile.absolutePath 9 | 10 | override val isDirectory: Boolean 11 | get() = this@toProjectFile.isDirectory 12 | 13 | override val children: List 14 | get() = this@toProjectFile 15 | .listFiles { _, name -> !name.startsWith(".") } 16 | .orEmpty() 17 | .map { it.toProjectFile() } 18 | 19 | private val numberOfFiles 20 | get() = listFiles()?.size ?: 0 21 | 22 | override val hasChildren: Boolean 23 | get() = isDirectory && numberOfFiles > 0 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/data/analytics/ModuleCreationAnalytics.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.data.analytics 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ModuleCreationAnalytics( 7 | val moduleType: String, 8 | val threeModule: Boolean, 9 | val addGitIgnore: Boolean, 10 | val addReadme: Boolean, 11 | val gradleNameToFollow: Boolean, 12 | val useKts: Boolean 13 | ) 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/data/analytics/ModuleCreationErrorAnalytics.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.data.analytics 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ModuleCreationErrorAnalytics( 7 | val message: String 8 | ) 9 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/file/FileWriter.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.file 2 | 3 | import com.joetr.modulemaker.ANDROID 4 | import com.joetr.modulemaker.MULTIPLATFORM 5 | import com.joetr.modulemaker.persistence.PreferenceService 6 | import com.joetr.modulemaker.template.GitIgnoreTemplate 7 | import com.joetr.modulemaker.template.TemplateWriter 8 | import java.io.File 9 | import java.io.Writer 10 | import java.nio.file.Files 11 | import java.nio.file.Paths 12 | import kotlin.io.path.absolutePathString 13 | 14 | const val ANDROID_KEY = "android" 15 | const val KOTLIN_KEY = "kotlin" 16 | const val GLUE_KEY = "glue" 17 | const val API_KEY = "api" 18 | const val IMPL_KEY = "key" 19 | 20 | /** 21 | * This class is responsible for writing files into the project 22 | */ 23 | class FileWriter( 24 | private val preferenceService: PreferenceService 25 | ) { 26 | 27 | private val templateWriter = TemplateWriter( 28 | preferenceService = preferenceService 29 | ) 30 | 31 | fun createModule( 32 | settingsGradleFile: File, 33 | workingDirectory: File, 34 | modulePathAsString: String, 35 | moduleType: String, 36 | showErrorDialog: (String) -> Unit, 37 | showSuccessDialog: () -> Unit, 38 | enhancedModuleCreationStrategy: Boolean, 39 | useKtsBuildFile: Boolean, 40 | gradleFileFollowModule: Boolean, 41 | packageName: String, 42 | addReadme: Boolean, 43 | addGitIgnore: Boolean, 44 | rootPathString: String, 45 | previewMode: Boolean = false, 46 | platformType: String = ANDROID, 47 | sourceSets: List = emptyList() 48 | ): List { 49 | val filesCreated = mutableListOf() 50 | 51 | val fileReady = modulePathAsString.replace(":", "/") 52 | 53 | val path = Paths.get(workingDirectory.toURI()) 54 | val modulePath = Paths.get(path.toString(), fileReady) 55 | val moduleFile = File(modulePath.absolutePathString()) 56 | 57 | // get the actual module name, not the path. at this point, it will be something like :experiences:foo 58 | val moduleName = modulePathAsString.split(":").last() 59 | 60 | if (moduleName.isEmpty()) { 61 | // display alert 62 | showErrorDialog("Module name empty / not as expected (is it formatted as :module?)") 63 | return emptyList() 64 | } 65 | 66 | if (previewMode.not()) { 67 | // create if it doesn't exist 68 | moduleFile.mkdirs() 69 | 70 | // add to settings.gradle.kts 71 | addToSettingsAtCorrectLocation( 72 | rootPathAsString = rootPathString, 73 | modulePathAsString = modulePathAsString, 74 | settingsGradleFile = settingsGradleFile, 75 | enhancedModuleCreationStrategy = enhancedModuleCreationStrategy, 76 | showErrorDialog = showErrorDialog 77 | ) 78 | } 79 | 80 | if (enhancedModuleCreationStrategy) { 81 | filesCreated += createEnhancedModuleStructure( 82 | moduleFile = moduleFile, 83 | moduleType = moduleType, 84 | useKtsBuildFile = useKtsBuildFile, 85 | gradleFileFollowModule = gradleFileFollowModule, 86 | packageName = packageName, 87 | addReadme = addReadme, 88 | addGitIgnore = addGitIgnore, 89 | previewMode = previewMode, 90 | platformType = platformType, 91 | sourceSets = sourceSets 92 | ) 93 | } else { 94 | filesCreated += createDefaultModuleStructure( 95 | moduleFile = moduleFile, 96 | moduleName = moduleName, 97 | moduleType = moduleType, 98 | useKtsBuildFile = useKtsBuildFile, 99 | gradleFileFollowModule = gradleFileFollowModule, 100 | packageName = packageName, 101 | addReadme = addReadme, 102 | addGitIgnore = addGitIgnore, 103 | previewMode = previewMode, 104 | platformType = platformType, 105 | sourceSets = sourceSets 106 | ) 107 | } 108 | 109 | if (previewMode.not()) { 110 | showSuccessDialog() 111 | } 112 | 113 | return filesCreated 114 | } 115 | 116 | private fun createEnhancedModuleStructure( 117 | moduleFile: File, 118 | moduleType: String, 119 | useKtsBuildFile: Boolean, 120 | gradleFileFollowModule: Boolean, 121 | packageName: String, 122 | addReadme: Boolean, 123 | addGitIgnore: Boolean, 124 | previewMode: Boolean, 125 | platformType: String, 126 | sourceSets: List 127 | ): List { 128 | val filesCreated = mutableListOf() 129 | 130 | // make the 3 module 131 | moduleFile.toPath().resolve(preferenceService.preferenceState.glueModuleName).toFile().apply { 132 | if (previewMode.not()) { 133 | mkdirs() 134 | } 135 | // create the gradle file 136 | filesCreated += templateWriter.createGradleFile( 137 | moduleFile = this, 138 | moduleName = moduleFile.path.split(File.separator).toList().last().plus("-") 139 | .plus(preferenceService.preferenceState.glueModuleName), 140 | moduleType = moduleType, 141 | useKtsBuildFile = useKtsBuildFile, 142 | defaultKey = GLUE_KEY, 143 | gradleFileFollowModule = gradleFileFollowModule, 144 | packageName = packageName.plus(".${preferenceService.preferenceState.glueModuleName}"), 145 | previewMode = previewMode, 146 | platformType = platformType 147 | ) 148 | 149 | // create default packages 150 | filesCreated += createDefaultPackages( 151 | moduleFile = this, 152 | packageName = packageName.plus(".${preferenceService.preferenceState.glueModuleName}"), 153 | previewMode = previewMode, 154 | platformType = platformType, 155 | sourceSets = sourceSets 156 | ) 157 | 158 | if (addGitIgnore) { 159 | filesCreated += createGitIgnore( 160 | moduleFile = this, 161 | previewMode = previewMode 162 | ) 163 | } 164 | } 165 | 166 | moduleFile.toPath().resolve(preferenceService.preferenceState.implModuleName).toFile().apply { 167 | if (previewMode.not()) { 168 | mkdirs() 169 | } 170 | filesCreated += templateWriter.createGradleFile( 171 | moduleFile = this, 172 | moduleName = moduleFile.path.split(File.separator).toList().last().plus("-") 173 | .plus(preferenceService.preferenceState.implModuleName), 174 | moduleType = moduleType, 175 | useKtsBuildFile = useKtsBuildFile, 176 | defaultKey = IMPL_KEY, 177 | gradleFileFollowModule = gradleFileFollowModule, 178 | packageName = packageName.plus(".${preferenceService.preferenceState.implModuleName}"), 179 | previewMode = previewMode, 180 | platformType = platformType 181 | ) 182 | 183 | // create default packages 184 | filesCreated += createDefaultPackages( 185 | moduleFile = this, 186 | packageName = packageName.plus(".${preferenceService.preferenceState.implModuleName}"), 187 | previewMode = previewMode, 188 | platformType = platformType, 189 | sourceSets = sourceSets 190 | ) 191 | 192 | if (addGitIgnore) { 193 | filesCreated += createGitIgnore( 194 | moduleFile = this, 195 | previewMode = previewMode 196 | ) 197 | } 198 | } 199 | 200 | moduleFile.toPath().resolve(preferenceService.preferenceState.apiModuleName).toFile().apply { 201 | if (previewMode.not()) { 202 | mkdirs() 203 | } 204 | filesCreated += templateWriter.createGradleFile( 205 | moduleFile = this, 206 | moduleName = moduleFile.path.split(File.separator).toList().last().plus("-") 207 | .plus(preferenceService.preferenceState.apiModuleName), 208 | moduleType = moduleType, 209 | useKtsBuildFile = useKtsBuildFile, 210 | defaultKey = API_KEY, 211 | gradleFileFollowModule = gradleFileFollowModule, 212 | packageName = packageName.plus(".${preferenceService.preferenceState.apiModuleName}"), 213 | previewMode = previewMode, 214 | platformType = platformType 215 | ) 216 | 217 | if (addReadme) { 218 | // create readme file for the api module 219 | filesCreated += templateWriter.createReadmeFile( 220 | moduleFile = this, 221 | moduleName = preferenceService.preferenceState.apiModuleName, 222 | previewMode = previewMode 223 | ) 224 | } 225 | 226 | // create default packages 227 | filesCreated += createDefaultPackages( 228 | moduleFile = this, 229 | packageName = packageName.plus(".${preferenceService.preferenceState.apiModuleName}"), 230 | previewMode = previewMode, 231 | platformType = platformType, 232 | sourceSets = sourceSets 233 | ) 234 | 235 | if (addGitIgnore) { 236 | filesCreated += createGitIgnore( 237 | moduleFile = this, 238 | previewMode = previewMode 239 | ) 240 | } 241 | } 242 | 243 | return filesCreated 244 | } 245 | 246 | private fun createDefaultModuleStructure( 247 | moduleFile: File, 248 | moduleName: String, 249 | moduleType: String, 250 | useKtsBuildFile: Boolean, 251 | gradleFileFollowModule: Boolean, 252 | packageName: String, 253 | addReadme: Boolean, 254 | addGitIgnore: Boolean, 255 | previewMode: Boolean, 256 | platformType: String, 257 | sourceSets: List 258 | ): List { 259 | val filesCreated = mutableListOf() 260 | 261 | // create gradle files 262 | filesCreated += templateWriter.createGradleFile( 263 | moduleFile = moduleFile, 264 | moduleName = moduleName, 265 | moduleType = moduleType, 266 | useKtsBuildFile = useKtsBuildFile, 267 | defaultKey = null, 268 | gradleFileFollowModule = gradleFileFollowModule, 269 | packageName = packageName, 270 | previewMode = previewMode, 271 | platformType = platformType 272 | ) 273 | 274 | if (addReadme) { 275 | // create readme file 276 | filesCreated += templateWriter.createReadmeFile( 277 | moduleFile = moduleFile, 278 | moduleName = moduleName, 279 | previewMode = previewMode 280 | ) 281 | } 282 | 283 | // create default packages 284 | filesCreated += createDefaultPackages( 285 | moduleFile = moduleFile, 286 | packageName = packageName, 287 | previewMode = previewMode, 288 | platformType = platformType, 289 | sourceSets = sourceSets 290 | ) 291 | 292 | if (addGitIgnore) { 293 | filesCreated += createGitIgnore( 294 | moduleFile = moduleFile, 295 | previewMode = previewMode 296 | ) 297 | } 298 | 299 | return filesCreated 300 | } 301 | 302 | private fun createGitIgnore(moduleFile: File, previewMode: Boolean): List { 303 | val gitignoreFile = Paths.get(moduleFile.absolutePath).toFile() 304 | 305 | val filePath = Paths.get(gitignoreFile.absolutePath, ".gitignore").toFile() 306 | 307 | if (previewMode.not()) { 308 | val writer: Writer = java.io.FileWriter(filePath) 309 | 310 | val customPreferences = preferenceService.preferenceState.gitignoreTemplate 311 | val dataToWrite = customPreferences.ifEmpty { 312 | GitIgnoreTemplate.data 313 | } 314 | 315 | writer.write(dataToWrite) 316 | writer.flush() 317 | writer.close() 318 | } 319 | 320 | return listOf(filePath) 321 | } 322 | 323 | /** 324 | * Creates the default package name 325 | * 326 | * Gives the module a src/main/kotlin folder with com. name 327 | */ 328 | private fun createDefaultPackages( 329 | moduleFile: File, 330 | packageName: String, 331 | previewMode: Boolean, 332 | platformType: String, 333 | sourceSets: List 334 | ): List { 335 | fun makePath(srcPath: File): File { 336 | val packagePath = Paths.get(srcPath.path, packageName.split(".").joinToString(File.separator)).toFile() 337 | // create default package 338 | val stringBuilder = StringBuilder() 339 | val filePath = Paths.get(srcPath.absolutePath, stringBuilder.toString()).toFile() 340 | if (previewMode.not()) { 341 | packagePath.mkdirs() 342 | filePath.mkdirs() 343 | } 344 | return packagePath 345 | } 346 | // create src/main 347 | val packagePaths = if (platformType == ANDROID) { 348 | val srcPath = Paths.get(moduleFile.absolutePath, "src/main/kotlin").toFile() 349 | val packagePath = makePath(srcPath) 350 | listOf(packagePath) 351 | } else if (platformType == MULTIPLATFORM) { 352 | val paths = mutableListOf() 353 | sourceSets.forEach { 354 | val srcPath = Paths.get(moduleFile.absolutePath, "src/$it/kotlin").toFile() 355 | val packagePath = makePath(srcPath) 356 | paths.add(packagePath) 357 | } 358 | paths 359 | } else { 360 | throw IllegalArgumentException("Unknown platform type $platformType") 361 | } 362 | 363 | return packagePaths 364 | } 365 | 366 | /** 367 | * Inserts the entry into settings.gradle.kts at the correct spot to maintain alphabetical order 368 | * 369 | * This assumes the file was in alphabetical order to begin with 370 | */ 371 | private fun addToSettingsAtCorrectLocation( 372 | settingsGradleFile: File, 373 | modulePathAsString: String, 374 | enhancedModuleCreationStrategy: Boolean, 375 | showErrorDialog: (String) -> Unit, 376 | rootPathAsString: String 377 | ) { 378 | val settingsFile = Files.readAllLines(Paths.get(settingsGradleFile.toURI())) 379 | 380 | // if the user has non-empty include keyword set, only check for that. 381 | val includeKeywords = if (preferenceService.preferenceState.includeProjectKeyword.isNotEmpty()) { 382 | listOf(preferenceService.preferenceState.includeProjectKeyword) 383 | } else { 384 | listOf( 385 | "includeProject", 386 | "includeBuild", 387 | "include" 388 | ) 389 | } 390 | 391 | val twoParametersPattern = """\(".+", ".+"\)""".toRegex() 392 | 393 | val lastNonEmptyLineInSettingsGradleFile = settingsFile.last { settingsFileLine -> 394 | settingsFileLine.isNotEmpty() && includeKeywords.any { 395 | settingsFileLine.contains(it) 396 | } 397 | } 398 | val projectIncludeKeyword = includeKeywords.firstOrNull { includeKeyword -> 399 | lastNonEmptyLineInSettingsGradleFile.contains(includeKeyword) 400 | } 401 | 402 | if (projectIncludeKeyword == null) { 403 | showErrorDialog("Could not find any include statements in settings.gradle(.kts) file") 404 | return 405 | } 406 | 407 | val usesTwoParameters = settingsFile.any { line -> 408 | twoParametersPattern.containsMatchIn(line) 409 | } 410 | 411 | // get the last line numbers for an include statement 412 | val lastLineNumberOfFirstIncludeProjectStatement = settingsFile.indexOfLast { 413 | settingsFileContainsSpecialIncludeKeyword(it, projectIncludeKeyword) 414 | } 415 | 416 | // traverse backwards from there to find the first instance 417 | var tempIndexForSettingsFile = lastLineNumberOfFirstIncludeProjectStatement 418 | while (tempIndexForSettingsFile >= 0) { 419 | val currentLine = settingsFile[tempIndexForSettingsFile] 420 | if (currentLine.trim().isEmpty() || settingsFileContainsSpecialIncludeKeyword( 421 | currentLine, 422 | projectIncludeKeyword 423 | ) 424 | ) { 425 | tempIndexForSettingsFile-- 426 | } else { 427 | break 428 | } 429 | } 430 | 431 | // assume tempIndexForSettingsFile is the first line 432 | val firstLineNumberOfFirstIncludeProjectStatement = tempIndexForSettingsFile + 1 433 | 434 | if (firstLineNumberOfFirstIncludeProjectStatement <= 0) { 435 | showErrorDialog("Could not find any include statements in settings.gradle(.kts) file") 436 | return 437 | } 438 | 439 | // sub list them and create a new list so we aren't modifying the original 440 | val includeProjectStatements = settingsFile.subList( 441 | firstLineNumberOfFirstIncludeProjectStatement, 442 | lastLineNumberOfFirstIncludeProjectStatement + 1 443 | ) 444 | .filter { 445 | it.isNotEmpty() 446 | } 447 | .toMutableList() 448 | 449 | val textToWrite = constructTextToWrite( 450 | enhancedModuleCreationStrategy = enhancedModuleCreationStrategy, 451 | usesTwoParameters = usesTwoParameters, 452 | projectIncludeKeyword = projectIncludeKeyword, 453 | modulePathAsString = modulePathAsString, 454 | rootPathAsString = rootPathAsString 455 | ) 456 | 457 | // the spot we want to insert it is the first line we find that is after it alphabetically 458 | val insertionIndex = includeProjectStatements.indexOfFirst { 459 | it.isNotEmpty() && it.lowercase() >= textToWrite.lowercase() 460 | } 461 | 462 | if (insertionIndex < 0) { 463 | /** 464 | * IN a scenario where there is just a single include statement, we want to differentiate between the two scenarios: 465 | * 466 | * include(":app") for example and 467 | * include( 468 | * ":app", 469 | * ":module1", 470 | * ) etc 471 | * 472 | * in the former case, we want to add a 1 offset to insert the new module after the single module 473 | * in the latter case, we don't really support that, but we also don't want to add it after the include statement to break 474 | * the current include, so we insert it just before 475 | */ 476 | val offsetAmount = if (includeProjectStatements.size == 1 && includeProjectStatements.first() 477 | .doesNotContainModule(projectIncludeKeyword) 478 | ) { 479 | 0 480 | } else { 481 | 1 482 | } 483 | // insert it at the end as nothing is past it 484 | settingsFile.add(lastLineNumberOfFirstIncludeProjectStatement + offsetAmount, textToWrite) 485 | } else { 486 | // insert it in our original list adding the original offset of the first line 487 | settingsFile.add(insertionIndex + firstLineNumberOfFirstIncludeProjectStatement, textToWrite) 488 | } 489 | 490 | Files.write(Paths.get(settingsGradleFile.toURI()), settingsFile) 491 | } 492 | 493 | private fun settingsFileContainsSpecialIncludeKeyword( 494 | stringToCheck: String, 495 | projectIncludeKeyword: String 496 | ): Boolean { 497 | return stringToCheck.contains("$projectIncludeKeyword(\"") || 498 | stringToCheck.contains("$projectIncludeKeyword('") || 499 | stringToCheck.contains("$projectIncludeKeyword(") || 500 | stringToCheck.contains("$projectIncludeKeyword \"") || 501 | stringToCheck.contains("$projectIncludeKeyword '") 502 | } 503 | 504 | private fun constructTextToWrite( 505 | enhancedModuleCreationStrategy: Boolean, 506 | usesTwoParameters: Boolean, 507 | projectIncludeKeyword: String, 508 | modulePathAsString: String, 509 | rootPathAsString: String 510 | ): String { 511 | fun buildText(path: String): String { 512 | val parametersString = if (usesTwoParameters) { 513 | val filePath = "$rootPathAsString${path.replace(":", File.separator)}".removePrefix("/") 514 | "\"$path\", \"$filePath\"" 515 | } else { 516 | "\"$path\"" 517 | } 518 | return "$projectIncludeKeyword($parametersString)" 519 | } 520 | 521 | return if (enhancedModuleCreationStrategy) { 522 | val paths = arrayOf( 523 | "$modulePathAsString:${preferenceService.preferenceState.apiModuleName}", 524 | "$modulePathAsString:${preferenceService.preferenceState.implModuleName}", 525 | "$modulePathAsString:${preferenceService.preferenceState.glueModuleName}" 526 | ) 527 | paths.joinToString("\n") { buildText(it) } 528 | } else { 529 | buildText(modulePathAsString) 530 | } 531 | } 532 | } 533 | 534 | private fun String.doesNotContainModule(includeKeyword: String): Boolean { 535 | return this.replace(" ", "").replace("(", "") == includeKeyword 536 | } 537 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/persistence/PreferenceService.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.persistence 2 | 3 | interface PreferenceService { 4 | var preferenceState: PreferenceServiceImpl.Companion.State 5 | } 6 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/persistence/PreferenceServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.persistence 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.components.PersistentStateComponent 5 | import com.intellij.openapi.components.State 6 | import com.intellij.openapi.components.Storage 7 | import com.intellij.util.xmlb.XmlSerializerUtil.copyBean 8 | import com.joetr.modulemaker.DEFAULT_ADD_GIT_IGNORE 9 | import com.joetr.modulemaker.DEFAULT_ADD_README 10 | import com.joetr.modulemaker.DEFAULT_API_MODULE_NAME 11 | import com.joetr.modulemaker.DEFAULT_BASE_PACKAGE_NAME 12 | import com.joetr.modulemaker.DEFAULT_GLUE_MODULE_NAME 13 | import com.joetr.modulemaker.DEFAULT_GRADLE_FILE_NAMED_AFTER_MODULE 14 | import com.joetr.modulemaker.DEFAULT_IMPL_MODULE_NAME 15 | import com.joetr.modulemaker.DEFAULT_INCLUDE_KEYWORD 16 | import com.joetr.modulemaker.DEFAULT_REFRESH_ON_MODULE_ADD 17 | import com.joetr.modulemaker.DEFAULT_THREE_MODULE_CREATION 18 | import com.joetr.modulemaker.DEFAULT_USE_KTS_FILE_EXTENSION 19 | import kotlinx.serialization.Serializable 20 | import org.jetbrains.annotations.Nullable 21 | 22 | @State(name = "PreferenceService", storages = [(Storage("module_maker_preferences.xml"))]) 23 | class PreferenceServiceImpl : PersistentStateComponent, PreferenceService { 24 | 25 | private var state = State() 26 | 27 | override var preferenceState: State 28 | get() = this.state 29 | set(value) { 30 | this.state = value 31 | } 32 | 33 | @Nullable 34 | override fun getState(): State { 35 | return this.preferenceState 36 | } 37 | 38 | override fun loadState(from: State) { 39 | copyBean(from, this.preferenceState) 40 | } 41 | 42 | companion object { 43 | 44 | @Serializable 45 | data class State( 46 | var androidTemplate: String = "", 47 | var kotlinTemplate: String = "", 48 | var multiplatformTemplate: String = "", 49 | var apiTemplate: String = "", 50 | var apiModuleName: String = DEFAULT_API_MODULE_NAME, 51 | var glueTemplate: String = "", 52 | var glueModuleName: String = DEFAULT_GLUE_MODULE_NAME, 53 | var implTemplate: String = "", 54 | var implModuleName: String = DEFAULT_IMPL_MODULE_NAME, 55 | var gitignoreTemplate: String = "", 56 | var packageName: String = DEFAULT_BASE_PACKAGE_NAME, 57 | var includeProjectKeyword: String = DEFAULT_INCLUDE_KEYWORD, 58 | var refreshOnModuleAdd: Boolean = DEFAULT_REFRESH_ON_MODULE_ADD, 59 | var threeModuleCreationDefault: Boolean = DEFAULT_THREE_MODULE_CREATION, 60 | var useKtsFileExtension: Boolean = DEFAULT_USE_KTS_FILE_EXTENSION, 61 | var gradleFileNamedAfterModule: Boolean = DEFAULT_GRADLE_FILE_NAMED_AFTER_MODULE, 62 | var addReadme: Boolean = DEFAULT_ADD_README, 63 | var addGitIgnore: Boolean = DEFAULT_ADD_GIT_IGNORE 64 | ) 65 | 66 | @JvmStatic 67 | val instance: PreferenceServiceImpl 68 | get() = ApplicationManager.getApplication().getService(PreferenceServiceImpl::class.java) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/template/AndroidModuleKtsTemplate.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.template 2 | 3 | object AndroidModuleKtsTemplate { 4 | val data = """ 5 | plugins { 6 | id("com.android.library") 7 | } 8 | 9 | android { 10 | namespace = "${'$'}{packageName}" 11 | } 12 | 13 | dependencies { 14 | 15 | } 16 | """.trimIndent() 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/template/AndroidModuleTemplate.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.template 2 | 3 | object AndroidModuleTemplate { 4 | val data = """ 5 | apply plugin: "com.android.library" 6 | apply plugin: "kotlin-android" 7 | 8 | android { 9 | namespace = "${'$'}{packageName}" 10 | } 11 | 12 | dependencies { 13 | 14 | } 15 | """.trimIndent() 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/template/GitIgnoreTemplate.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.template 2 | 3 | object GitIgnoreTemplate { 4 | val data = """ 5 | # Gradle files 6 | .gradle/ 7 | build/ 8 | 9 | # Local configuration file (sdk path, etc) 10 | local.properties 11 | 12 | # Log/OS Files 13 | *.log 14 | 15 | # Android Studio generated files and folders 16 | captures/ 17 | .externalNativeBuild/ 18 | .cxx/ 19 | *.apk 20 | output.json 21 | 22 | # IntelliJ 23 | *.iml 24 | .idea/ 25 | misc.xml 26 | deploymentTargetDropDown.xml 27 | render.experimental.xml 28 | 29 | # Keystore files 30 | *.jks 31 | *.keystore 32 | 33 | # Google Services (e.g. APIs or Firebase) 34 | google-services.json 35 | 36 | # Android Profiling 37 | *.hprof 38 | """.trimIndent() 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/template/KotlinModuleKtsTemplate.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.template 2 | 3 | object KotlinModuleKtsTemplate { 4 | val data = """ 5 | plugins { 6 | "kotlin" 7 | } 8 | 9 | dependencies { 10 | 11 | } 12 | """.trimIndent() 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/template/KotlinModuleTemplate.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.template 2 | 3 | object KotlinModuleTemplate { 4 | val data = """ 5 | apply plugin: "kotlin" 6 | 7 | dependencies { 8 | 9 | } 10 | """.trimIndent() 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/template/ModuleReadMeTemplate.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.template 2 | 3 | object ModuleReadMeTemplate { 4 | val data = """ 5 | # ${'$'}{moduleName} 6 | 7 | TODO 8 | """.trimIndent() 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/template/MultiplatformKtsTemplate.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.template 2 | 3 | object MultiplatformKtsTemplate { 4 | val data = """ 5 | plugins { 6 | kotlin("multiplatform") 7 | kotlin("plugin.compose") 8 | id("com.android.library") 9 | id("org.jetbrains.compose") 10 | } 11 | 12 | version = "1.0-SNAPSHOT" 13 | 14 | kotlin { 15 | androidTarget() 16 | jvm("desktop") 17 | js { 18 | browser() 19 | useEsModules() 20 | } 21 | wasmJs { browser() } 22 | 23 | listOf( 24 | iosX64(), 25 | iosArm64(), 26 | iosSimulatorArm64() 27 | ).forEach { iosTarget -> 28 | iosTarget.binaries.framework { 29 | baseName = "shared" 30 | isStatic = true 31 | } 32 | } 33 | 34 | applyDefaultHierarchyTemplate() 35 | 36 | sourceSets { 37 | all { 38 | languageSettings { 39 | optIn("org.jetbrains.compose.resources.ExperimentalResourceApi") 40 | } 41 | } 42 | 43 | commonMain.dependencies { 44 | implementation(compose.runtime) 45 | implementation(compose.foundation) 46 | implementation(compose.material) 47 | implementation(compose.components.resources) 48 | } 49 | 50 | androidMain.dependencies { 51 | 52 | } 53 | 54 | val jsWasmMain by creating { 55 | dependsOn(commonMain.get()) 56 | } 57 | 58 | val jsMain by getting { 59 | dependsOn(jsWasmMain) 60 | } 61 | 62 | val wasmJsMain by getting { 63 | dependsOn(jsWasmMain) 64 | } 65 | 66 | val desktopMain by getting 67 | desktopMain.dependencies { 68 | implementation(compose.desktop.common) 69 | 70 | } 71 | val desktopTest by getting 72 | desktopTest.dependencies { 73 | implementation(compose.desktop.currentOs) 74 | implementation(compose.desktop.uiTestJUnit4) 75 | } 76 | } 77 | } 78 | """.trimIndent() 79 | } 80 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/template/TemplateVariable.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.template 2 | 3 | enum class TemplateVariable(val templateVariable: String) { 4 | PACKAGE_NAME( 5 | """ 6 | "${'$'}{packageName}" 7 | """.trimIndent() 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/template/TemplateWriter.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.template 2 | 3 | import com.joetr.modulemaker.ANDROID 4 | import com.joetr.modulemaker.FREEMARKER_VERSION 5 | import com.joetr.modulemaker.KOTLIN 6 | import com.joetr.modulemaker.MULTIPLATFORM 7 | import com.joetr.modulemaker.file.ANDROID_KEY 8 | import com.joetr.modulemaker.file.API_KEY 9 | import com.joetr.modulemaker.file.GLUE_KEY 10 | import com.joetr.modulemaker.file.IMPL_KEY 11 | import com.joetr.modulemaker.file.KOTLIN_KEY 12 | import com.joetr.modulemaker.persistence.PreferenceService 13 | import freemarker.template.Configuration 14 | import freemarker.template.Template 15 | import freemarker.template.TemplateException 16 | import java.io.File 17 | import java.io.FileWriter 18 | import java.io.IOException 19 | import java.io.Writer 20 | import java.nio.file.Paths 21 | 22 | class TemplateWriter( 23 | private val preferenceService: PreferenceService 24 | ) { 25 | 26 | private val cfg = Configuration(FREEMARKER_VERSION).apply { 27 | setClassLoaderForTemplateLoading(TemplateWriter::class.java.classLoader, "") 28 | } 29 | 30 | /** 31 | * Creates gradle file for the module from base gradle template file 32 | */ 33 | fun createGradleFile( 34 | moduleFile: File, 35 | moduleName: String, 36 | moduleType: String, 37 | useKtsBuildFile: Boolean, 38 | defaultKey: String?, 39 | gradleFileFollowModule: Boolean, 40 | packageName: String, 41 | previewMode: Boolean, 42 | platformType: String 43 | ): List { 44 | try { 45 | // Build the data-model 46 | val data: MutableMap = HashMap() 47 | data["packageName"] = packageName 48 | 49 | // load gradle file from template folder 50 | val gradleTemplate: Template = when (moduleType) { 51 | KOTLIN -> { 52 | val customPreferences = getPreferenceFromKey(defaultKey, if (platformType == MULTIPLATFORM) MULTIPLATFORM else KOTLIN_KEY) 53 | if (customPreferences.isNotEmpty()) { 54 | Template( 55 | null, 56 | customPreferences, 57 | cfg 58 | ) 59 | } else { 60 | val template = if (useKtsBuildFile) { 61 | KotlinModuleKtsTemplate.data 62 | } else { 63 | KotlinModuleTemplate.data 64 | } 65 | Template( 66 | null, 67 | template, 68 | cfg 69 | ) 70 | } 71 | } 72 | ANDROID -> { 73 | val customPreferences = getPreferenceFromKey(defaultKey, if (platformType == MULTIPLATFORM) MULTIPLATFORM else ANDROID_KEY) 74 | 75 | if (customPreferences.isNotEmpty()) { 76 | Template( 77 | null, 78 | customPreferences, 79 | cfg 80 | ) 81 | } else { 82 | val template = if (platformType == ANDROID) { 83 | if (useKtsBuildFile) { 84 | AndroidModuleKtsTemplate.data 85 | } else { 86 | AndroidModuleTemplate.data 87 | } 88 | } else if (platformType == MULTIPLATFORM) { 89 | MultiplatformKtsTemplate.data 90 | } else { 91 | throw IllegalArgumentException("Unknown platform type $platformType") 92 | } 93 | Template( 94 | null, 95 | template, 96 | cfg 97 | ) 98 | } 99 | } 100 | else -> throw IllegalArgumentException("Unknown module type") 101 | } 102 | 103 | // File output 104 | val extension = if (useKtsBuildFile) { 105 | ".gradle.kts" 106 | } else { 107 | ".gradle" 108 | } 109 | val fileName = if (gradleFileFollowModule) { 110 | moduleName.plus(extension) 111 | } else { 112 | "build".plus(extension) 113 | } 114 | 115 | val filePath = Paths.get(moduleFile.absolutePath, fileName).toFile() 116 | 117 | if (previewMode.not()) { 118 | val file: Writer = FileWriter(Paths.get(moduleFile.absolutePath, fileName).toFile()) 119 | gradleTemplate.process(data, file) 120 | file.flush() 121 | file.close() 122 | } 123 | 124 | return listOf(filePath) 125 | } catch (e: IOException) { 126 | e.printStackTrace() 127 | } catch (e: TemplateException) { 128 | e.printStackTrace() 129 | } 130 | 131 | return emptyList() 132 | } 133 | 134 | fun createReadmeFile(moduleFile: File, moduleName: String, previewMode: Boolean): List { 135 | try { 136 | val manifestTemplate = Template( 137 | null, 138 | ModuleReadMeTemplate.data, 139 | cfg 140 | ) 141 | 142 | val data: MutableMap = HashMap() 143 | 144 | data["moduleName"] = moduleName 145 | 146 | // create directory for the readme 147 | val manifestFile = Paths.get(moduleFile.absolutePath).toFile() 148 | 149 | val filePath = Paths.get(manifestFile.absolutePath, "README.md").toFile() 150 | 151 | if (previewMode.not()) { 152 | manifestFile.mkdirs() 153 | 154 | // File output 155 | val file: Writer = FileWriter(filePath) 156 | manifestTemplate.process(data, file) 157 | file.flush() 158 | file.close() 159 | } 160 | 161 | return listOf(filePath) 162 | } catch (e: IOException) { 163 | e.printStackTrace() 164 | } catch (e: TemplateException) { 165 | e.printStackTrace() 166 | } 167 | 168 | return emptyList() 169 | } 170 | 171 | private fun getPreferenceFromKey(key: String?, fallback: String): String { 172 | return when (key ?: fallback) { 173 | IMPL_KEY -> preferenceService.preferenceState.implTemplate 174 | API_KEY -> preferenceService.preferenceState.apiTemplate 175 | GLUE_KEY -> preferenceService.preferenceState.glueTemplate 176 | ANDROID_KEY -> preferenceService.preferenceState.androidTemplate 177 | MULTIPLATFORM -> preferenceService.preferenceState.multiplatformTemplate 178 | KOTLIN_KEY -> preferenceService.preferenceState.kotlinTemplate 179 | else -> "" 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/ui/LabelledCheckbox.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.ui 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.Checkbox 8 | import androidx.compose.material.CheckboxDefaults 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.unit.dp 15 | 16 | @Composable 17 | fun LabelledCheckbox( 18 | modifier: Modifier = Modifier, 19 | label: String, 20 | checked: Boolean, 21 | onCheckedChange: (Boolean) -> Unit 22 | ) { 23 | Row( 24 | modifier = modifier.clickable { 25 | onCheckedChange(checked.not()) 26 | }.padding(end = 8.dp), 27 | horizontalArrangement = Arrangement.Start, 28 | verticalAlignment = Alignment.CenterVertically 29 | ) { 30 | Checkbox( 31 | checked = checked, 32 | onCheckedChange = { 33 | onCheckedChange(it) 34 | }, 35 | enabled = true, 36 | colors = CheckboxDefaults.colors(checkedColor = MaterialTheme.colors.primary, uncheckedColor = MaterialTheme.colors.primaryVariant) 37 | ) 38 | Text(text = label) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/ui/file/FileTree.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.ui.file 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import com.joetr.modulemaker.data.File 7 | 8 | class ExpandableFile( 9 | val file: File, 10 | val level: Int 11 | ) { 12 | var children: List by mutableStateOf(emptyList()) 13 | val canExpand: Boolean get() = file.hasChildren 14 | 15 | fun toggleExpanded() { 16 | children = if (children.isEmpty()) { 17 | file.children 18 | .map { ExpandableFile(it, level + 1) } 19 | .sortedWith(compareBy({ it.file.isDirectory }, { it.file.name })) 20 | .sortedBy { !it.file.isDirectory } 21 | } else { 22 | emptyList() 23 | } 24 | } 25 | } 26 | 27 | class FileTree(root: File) { 28 | 29 | private val expandableRoot = ExpandableFile(root, 0).apply { 30 | toggleExpanded() 31 | } 32 | 33 | val items: List get() = expandableRoot.toItems() 34 | 35 | inner class Item( 36 | internal val file: ExpandableFile 37 | ) { 38 | val name: String get() = file.file.name 39 | 40 | val level: Int get() = file.level 41 | 42 | val type: ItemType 43 | get() = if (file.file.isDirectory) { 44 | ItemType.Folder(isExpanded = file.children.isNotEmpty(), canExpand = file.canExpand) 45 | } else { 46 | ItemType.File(ext = file.file.name.substringAfterLast(".").lowercase()) 47 | } 48 | 49 | fun open() = when (type) { 50 | is ItemType.Folder -> file.toggleExpanded() 51 | is ItemType.File -> Unit 52 | } 53 | } 54 | 55 | sealed class ItemType { 56 | class Folder(val isExpanded: Boolean, val canExpand: Boolean) : ItemType() 57 | class File(val ext: String) : ItemType() 58 | } 59 | 60 | private fun ExpandableFile.toItems(): List { 61 | fun ExpandableFile.addTo(list: MutableList) { 62 | list.add(Item(this)) 63 | for (child in children) { 64 | child.addTo(list) 65 | } 66 | } 67 | 68 | val list = mutableListOf() 69 | addTo(list) 70 | return list 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/ui/file/FileTreeView.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.ui.file 2 | 3 | import androidx.compose.foundation.HorizontalScrollbar 4 | import androidx.compose.foundation.VerticalScrollbar 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.horizontalScroll 7 | import androidx.compose.foundation.hoverable 8 | import androidx.compose.foundation.interaction.MutableInteractionSource 9 | import androidx.compose.foundation.interaction.collectIsHoveredAsState 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.Row 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.fillMaxWidth 14 | import androidx.compose.foundation.layout.height 15 | import androidx.compose.foundation.layout.padding 16 | import androidx.compose.foundation.layout.size 17 | import androidx.compose.foundation.layout.wrapContentHeight 18 | import androidx.compose.foundation.lazy.LazyColumn 19 | import androidx.compose.foundation.lazy.rememberLazyListState 20 | import androidx.compose.foundation.rememberScrollState 21 | import androidx.compose.foundation.rememberScrollbarAdapter 22 | import androidx.compose.material.Icon 23 | import androidx.compose.material.LocalContentColor 24 | import androidx.compose.material.Surface 25 | import androidx.compose.material.Text 26 | import androidx.compose.material.icons.Icons 27 | import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight 28 | import androidx.compose.material.icons.automirrored.filled.Launch 29 | import androidx.compose.material.icons.automirrored.filled.TextSnippet 30 | import androidx.compose.material.icons.filled.BrokenImage 31 | import androidx.compose.material.icons.filled.Build 32 | import androidx.compose.material.icons.filled.Code 33 | import androidx.compose.material.icons.filled.Description 34 | import androidx.compose.material.icons.filled.KeyboardArrowDown 35 | import androidx.compose.material.icons.filled.KeyboardArrowRight 36 | import androidx.compose.material.icons.filled.Launch 37 | import androidx.compose.material.icons.filled.Settings 38 | import androidx.compose.material.icons.filled.TextSnippet 39 | import androidx.compose.runtime.Composable 40 | import androidx.compose.runtime.getValue 41 | import androidx.compose.runtime.remember 42 | import androidx.compose.ui.Alignment 43 | import androidx.compose.ui.Modifier 44 | import androidx.compose.ui.draw.clipToBounds 45 | import androidx.compose.ui.graphics.Color 46 | import androidx.compose.ui.platform.LocalDensity 47 | import androidx.compose.ui.text.style.TextOverflow 48 | import androidx.compose.ui.unit.Dp 49 | import androidx.compose.ui.unit.TextUnit 50 | import androidx.compose.ui.unit.dp 51 | import androidx.compose.ui.unit.sp 52 | 53 | @Composable 54 | fun FileTreeView(model: FileTree, height: Dp, onClick: (ExpandableFile) -> Unit, modifier: Modifier) = Surface( 55 | modifier = modifier.height(height) 56 | ) { 57 | with(LocalDensity.current) { 58 | Box { 59 | val lazyListState = rememberLazyListState() 60 | val scrollState = rememberScrollState() 61 | 62 | LazyColumn( 63 | modifier = Modifier.fillMaxSize().horizontalScroll(scrollState), 64 | state = lazyListState 65 | ) { 66 | items(model.items.size) { 67 | FileTreeItemView( 68 | 14.sp, 69 | 14.sp.toDp() * 1.5f, 70 | model.items[it], 71 | onClick = onClick, 72 | // if it's the last one and the scrollbar is showing 73 | showBottomPadding = it == model.items.size - 1 && (lazyListState.canScrollForward || lazyListState.canScrollBackward), 74 | // if the scrollbar is showing 75 | showEndPadding = scrollState.canScrollForward || scrollState.canScrollBackward 76 | ) 77 | } 78 | } 79 | 80 | VerticalScrollbar( 81 | rememberScrollbarAdapter(lazyListState), 82 | Modifier.align(Alignment.CenterEnd) 83 | ) 84 | 85 | HorizontalScrollbar( 86 | rememberScrollbarAdapter(scrollState), 87 | Modifier.align(Alignment.BottomStart) 88 | ) 89 | } 90 | } 91 | } 92 | 93 | @Composable 94 | private fun FileTreeItemView( 95 | fontSize: TextUnit, 96 | height: Dp, 97 | model: FileTree.Item, 98 | onClick: (ExpandableFile) -> Unit, 99 | showBottomPadding: Boolean, 100 | showEndPadding: Boolean 101 | ) = 102 | Row( 103 | modifier = Modifier 104 | .wrapContentHeight() 105 | .clickable { 106 | model.open() 107 | 108 | // let UI know 109 | onClick(model.file) 110 | } 111 | // give padding for scroll bar 112 | .padding( 113 | start = 24.dp * model.level, 114 | end = if (showEndPadding) 8.dp else 0.dp, 115 | bottom = if (showBottomPadding) 8.dp else 0.dp 116 | ) 117 | .height(height) 118 | .fillMaxWidth() 119 | ) { 120 | val interactionSource = remember { MutableInteractionSource() } 121 | val active by interactionSource.collectIsHoveredAsState() 122 | 123 | FileItemIcon(Modifier.align(Alignment.CenterVertically), model) 124 | Text( 125 | text = model.name, 126 | color = if (active) LocalContentColor.current.copy(alpha = 0.60f) else LocalContentColor.current, 127 | modifier = Modifier 128 | .align(Alignment.CenterVertically) 129 | .clipToBounds() 130 | .hoverable(interactionSource), 131 | softWrap = true, 132 | fontSize = fontSize, 133 | overflow = TextOverflow.Ellipsis, 134 | maxLines = 1 135 | ) 136 | } 137 | 138 | @Composable 139 | private fun FileItemIcon(modifier: Modifier, model: FileTree.Item) = Box(modifier.size(24.dp).padding(4.dp)) { 140 | when (val type = model.type) { 141 | is FileTree.ItemType.Folder -> when { 142 | !type.canExpand -> Unit 143 | type.isExpanded -> Icon( 144 | Icons.Default.KeyboardArrowDown, 145 | contentDescription = null, 146 | tint = LocalContentColor.current 147 | ) 148 | 149 | else -> Icon( 150 | Icons.AutoMirrored.Filled.KeyboardArrowRight, 151 | contentDescription = null, 152 | tint = LocalContentColor.current 153 | ) 154 | } 155 | 156 | is FileTree.ItemType.File -> when (type.ext) { 157 | in sourceCodeFileExtensions -> Icon( 158 | Icons.Default.Code, 159 | contentDescription = null, 160 | tint = Color(0xFF3E86A0) 161 | ) 162 | "txt" -> Icon(Icons.Default.Description, contentDescription = null, tint = Color(0xFF87939A)) 163 | "md" -> Icon(Icons.Default.Description, contentDescription = null, tint = Color(0xFF87939A)) 164 | "gitignore" -> Icon( 165 | Icons.Default.BrokenImage, 166 | contentDescription = null, 167 | tint = Color(0xFF87939A) 168 | ) 169 | "gradle" -> Icon(Icons.Default.Build, contentDescription = null, tint = Color(0xFF87939A)) 170 | "kts" -> Icon(Icons.Default.Build, contentDescription = null, tint = Color(0xFF3E86A0)) 171 | "properties" -> Icon( 172 | Icons.Default.Settings, 173 | contentDescription = null, 174 | tint = Color(0xFF62B543) 175 | ) 176 | "bat" -> Icon(Icons.AutoMirrored.Filled.Launch, contentDescription = null, tint = Color(0xFF87939A)) 177 | else -> Icon(Icons.AutoMirrored.Filled.TextSnippet, contentDescription = null, tint = Color(0xFF87939A)) 178 | } 179 | } 180 | } 181 | 182 | private val sourceCodeFileExtensions = listOf( 183 | "java", 184 | "kt", 185 | "cpp", 186 | "c", 187 | "h", 188 | "py", 189 | "js", 190 | "html", 191 | "css", 192 | "php", 193 | "rb", 194 | "swift", 195 | "go", 196 | "scala", 197 | "rust", 198 | "dart", 199 | "lua", 200 | "xml", 201 | "pl", 202 | "sh", 203 | "sql" 204 | ) 205 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val blue200 = Color(0xff90CAF9) 6 | val blue500 = Color(0xff2196F3) 7 | val blue700 = Color(0xff1976D2) 8 | 9 | val teal200 = Color(0xff80deea) 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.text.TextStyle 6 | import androidx.compose.ui.text.font.FontFamily 7 | import androidx.compose.ui.text.font.FontWeight 8 | import androidx.compose.ui.unit.sp 9 | 10 | val typography = Typography( 11 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ), 16 | body2 = TextStyle( 17 | fontFamily = FontFamily.Default, 18 | fontWeight = FontWeight.Normal, 19 | fontSize = 14.sp 20 | ), 21 | button = TextStyle( 22 | fontFamily = FontFamily.Default, 23 | fontWeight = FontWeight.W500, 24 | fontSize = 14.sp 25 | ), 26 | caption = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Normal, 29 | fontSize = 12.sp 30 | ), 31 | subtitle1 = TextStyle( 32 | fontFamily = FontFamily.Default, 33 | fontWeight = FontWeight.Normal, 34 | fontSize = 16.sp, 35 | color = Color.Gray 36 | ), 37 | subtitle2 = TextStyle( 38 | fontFamily = FontFamily.Default, 39 | fontWeight = FontWeight.Normal, 40 | fontSize = 14.sp, 41 | color = Color.Gray 42 | ) 43 | ) 44 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/ui/theme/WidgetTheme.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.ui.theme 2 | 3 | import androidx.compose.material.MaterialTheme 4 | import androidx.compose.material.Surface 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.graphics.Color 9 | import com.joetr.modulemaker.ui.theme.intellij.SwingColor 10 | import kotlinx.serialization.json.JsonNull.content 11 | 12 | private val DarkGreenColorPalette = darkColors( 13 | primary = blue200, 14 | primaryVariant = blue700, 15 | secondary = teal200, 16 | onPrimary = Color.Black, 17 | onSecondary = Color.White, 18 | error = Color.Red 19 | ) 20 | 21 | private val LightGreenColorPalette = lightColors( 22 | primary = blue500, 23 | primaryVariant = blue700, 24 | secondary = teal200, 25 | onPrimary = Color.White, 26 | onSurface = Color.Black 27 | ) 28 | 29 | @Composable 30 | fun WidgetTheme( 31 | darkTheme: Boolean = false, 32 | content: @Composable() 33 | () -> Unit 34 | ) { 35 | val colors = if (darkTheme) DarkGreenColorPalette else LightGreenColorPalette 36 | val swingColor = SwingColor() 37 | 38 | MaterialTheme( 39 | colors = colors.copy( 40 | background = swingColor.background, 41 | onBackground = swingColor.onBackground, 42 | surface = swingColor.background, 43 | onSurface = swingColor.onBackground 44 | ), 45 | typography = typography, 46 | shapes = shapes 47 | ) { 48 | Surface { 49 | content() 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/ui/theme/intellij/SwingColor.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.ui.theme.intellij 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.compose.runtime.MutableState 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.graphics.Color 9 | import com.intellij.ide.ui.LafManagerListener 10 | import com.intellij.openapi.application.ApplicationManager 11 | import javax.swing.UIManager 12 | import java.awt.Color as AWTColor 13 | 14 | interface SwingColor { 15 | val background: Color 16 | val onBackground: Color 17 | } 18 | 19 | @Composable 20 | fun SwingColor(): SwingColor { 21 | val swingColor = remember { SwingColorImpl() } 22 | 23 | val messageBus = remember { 24 | ApplicationManager.getApplication().messageBus.connect() 25 | } 26 | 27 | remember(messageBus) { 28 | messageBus.subscribe( 29 | LafManagerListener.TOPIC, 30 | ThemeChangeListener(swingColor::updateCurrentColors) 31 | ) 32 | } 33 | 34 | DisposableEffect(messageBus) { 35 | onDispose { 36 | messageBus.disconnect() 37 | } 38 | } 39 | 40 | return swingColor 41 | } 42 | 43 | private class SwingColorImpl : SwingColor { 44 | private val _backgroundState: MutableState = mutableStateOf(getBackgroundColor) 45 | private val _onBackgroundState: MutableState = mutableStateOf(getOnBackgroundColor) 46 | 47 | override val background: Color get() = _backgroundState.value 48 | override val onBackground: Color get() = _onBackgroundState.value 49 | 50 | private val getBackgroundColor get() = getColor(BACKGROUND_KEY) 51 | private val getOnBackgroundColor get() = getColor(ON_BACKGROUND_KEY) 52 | 53 | fun updateCurrentColors() { 54 | _backgroundState.value = getBackgroundColor 55 | _onBackgroundState.value = getOnBackgroundColor 56 | } 57 | 58 | private val AWTColor.asComposeColor: Color get() = Color(red, green, blue, alpha) 59 | private fun getColor(key: String): Color = UIManager.getColor(key).asComposeColor 60 | 61 | companion object { 62 | private const val BACKGROUND_KEY = "Panel.background" 63 | private const val ON_BACKGROUND_KEY = "Panel.foreground" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/kotlin/com/joetr/modulemaker/ui/theme/intellij/ThemeChangeListener.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.ui.theme.intellij 2 | 3 | import com.intellij.ide.ui.LafManager 4 | import com.intellij.ide.ui.LafManagerListener 5 | 6 | internal class ThemeChangeListener( 7 | val updateColors: () -> Unit 8 | ) : LafManagerListener { 9 | override fun lookAndFeelChanged(source: LafManager) { 10 | updateColors() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ccom.joetr.modulemaker 5 | Module Maker 6 | https://joetr.com 7 | 8 | com.intellij.modules.lang 9 | 10 | Enables the creation of modules with sensible defaults. 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/test/kotlin/com/joetr/modulemaker/AndroidModuleMakerTest.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker 2 | 3 | import com.joetr.modulemaker.file.FileWriter 4 | import com.joetr.modulemaker.persistence.PreferenceService 5 | import com.joetr.modulemaker.persistence.PreferenceServiceImpl 6 | import com.joetr.modulemaker.template.GitIgnoreTemplate 7 | import org.junit.Assert.assertEquals 8 | import org.junit.Assert.fail 9 | import org.junit.Before 10 | import org.junit.Rule 11 | import org.junit.Test 12 | import org.junit.rules.TemporaryFolder 13 | import java.io.File 14 | 15 | class AndroidModuleMakerTest { 16 | 17 | @JvmField 18 | @Rule 19 | var folder = TemporaryFolder() 20 | 21 | var testState = PreferenceServiceImpl.Companion.State() 22 | 23 | private val fakePreferenceService = object : PreferenceService { 24 | override var preferenceState: PreferenceServiceImpl.Companion.State 25 | get() = testState 26 | set(value) { 27 | testState = value 28 | } 29 | } 30 | 31 | private val fileWriter = FileWriter( 32 | preferenceService = fakePreferenceService 33 | ) 34 | 35 | private lateinit var settingsGradleFile: File 36 | 37 | @Before 38 | fun before() { 39 | settingsGradleFile = folder.populateSettingsGradleKtsWithFakeData() 40 | } 41 | 42 | @Test 43 | fun `android module created successfully`() { 44 | val modulePath = ":repository" 45 | val modulePathAsFile = "repository" 46 | 47 | fileWriter.createModule( 48 | settingsGradleFile = settingsGradleFile, 49 | workingDirectory = folder.root, 50 | modulePathAsString = modulePath, 51 | moduleType = ANDROID, 52 | showErrorDialog = { 53 | fail("No errors should be thrown") 54 | }, 55 | showSuccessDialog = { 56 | assert(true) 57 | }, 58 | enhancedModuleCreationStrategy = false, 59 | useKtsBuildFile = false, 60 | gradleFileFollowModule = false, 61 | packageName = testPackageName, 62 | addReadme = true, 63 | addGitIgnore = false, 64 | rootPathString = folder.root.toString(), 65 | previewMode = false 66 | 67 | ) 68 | 69 | // assert it was added to settings.gradle 70 | val settingsGradleFileContents = readFromFile(file = settingsGradleFile) 71 | assert( 72 | settingsGradleFileContents.contains("include(\":repository\")") 73 | ) 74 | 75 | // assert readme was generated 76 | assert( 77 | // root/repository/README.md 78 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + readmeFile).exists() 79 | ) 80 | 81 | // assert build.gradle is generated 82 | val buildGradleFile = File(folder.root.path + File.separator + modulePathAsFile + File.separator + buildGradleFileName) 83 | assert( 84 | // root/repository/build.gradle 85 | buildGradleFile.exists() 86 | ) 87 | 88 | // assert package name is included in build.gradle 89 | val buildGradleFileContents = readFromFile(buildGradleFile) 90 | assert( 91 | buildGradleFileContents.contains( 92 | " namespace = \"$testPackageName\"" 93 | ) 94 | ) 95 | 96 | // assert the correct package structure is generated 97 | assert( 98 | // root/repository/build.gradle 99 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "src/main/kotlin/com/joetr/test").exists() 100 | ) 101 | } 102 | 103 | @Test 104 | fun `when a template is set, that is used instead of default for creating build gradle`() { 105 | val modulePath = ":repository" 106 | val modulePathAsFile = "repository" 107 | val template = "test template" 108 | 109 | fakePreferenceService.preferenceState.androidTemplate = template 110 | 111 | fileWriter.createModule( 112 | settingsGradleFile = settingsGradleFile, 113 | workingDirectory = folder.root, 114 | modulePathAsString = modulePath, 115 | moduleType = ANDROID, 116 | showErrorDialog = { 117 | fail("No errors should be thrown") 118 | }, 119 | showSuccessDialog = { 120 | assert(true) 121 | }, 122 | enhancedModuleCreationStrategy = false, 123 | useKtsBuildFile = false, 124 | gradleFileFollowModule = false, 125 | packageName = testPackageName, 126 | addReadme = false, 127 | addGitIgnore = false, 128 | rootPathString = folder.root.toString(), 129 | previewMode = false 130 | ) 131 | 132 | // assert build.gradle is generated 133 | val buildGradleFile = File(folder.root.path + File.separator + modulePathAsFile + File.separator + buildGradleFileName) 134 | assert( 135 | // root/repository/build.gradle 136 | buildGradleFile.exists() 137 | ) 138 | 139 | // assert package name is included in build.gradle 140 | val buildGradleFileContents = readFromFile(buildGradleFile) 141 | assert( 142 | buildGradleFileContents.contains( 143 | template 144 | ) 145 | ) 146 | } 147 | 148 | @Test 149 | fun `when a template is set, the package name variable is replaced`() { 150 | val modulePath = ":repository" 151 | val modulePathAsFile = "repository" 152 | 153 | val template = """ 154 | this is a custom template 155 | 156 | android { 157 | namespace = "${'$'}{packageName}" 158 | } 159 | """.trimIndent() 160 | 161 | fakePreferenceService.preferenceState.androidTemplate = template 162 | 163 | fileWriter.createModule( 164 | settingsGradleFile = settingsGradleFile, 165 | workingDirectory = folder.root, 166 | modulePathAsString = modulePath, 167 | moduleType = ANDROID, 168 | showErrorDialog = { 169 | fail("No errors should be thrown") 170 | }, 171 | showSuccessDialog = { 172 | assert(true) 173 | }, 174 | enhancedModuleCreationStrategy = false, 175 | useKtsBuildFile = false, 176 | gradleFileFollowModule = false, 177 | packageName = testPackageName, 178 | addReadme = false, 179 | addGitIgnore = false, 180 | rootPathString = folder.root.toString(), 181 | previewMode = false 182 | 183 | ) 184 | 185 | // assert build.gradle file exists and contains the package name when using a custom template 186 | val buildGradleFile = File(folder.root.path + File.separator + modulePathAsFile + File.separator + buildGradleFileName) 187 | 188 | assert(buildGradleFile.exists()) 189 | 190 | val buildGradleFileContents = readFromFile(buildGradleFile) 191 | 192 | assert( 193 | buildGradleFileContents.contains( 194 | " namespace = \"$testPackageName\"" 195 | ) 196 | ) 197 | } 198 | 199 | @Test 200 | fun `android module created successfully when using nested modules`() { 201 | val modulePath = ":repository:database" 202 | val modulePathAsFile = "repository/database" 203 | 204 | fileWriter.createModule( 205 | settingsGradleFile = settingsGradleFile, 206 | workingDirectory = folder.root, 207 | modulePathAsString = modulePath, 208 | moduleType = ANDROID, 209 | showErrorDialog = { 210 | fail("No errors should be thrown") 211 | }, 212 | showSuccessDialog = { 213 | assert(true) 214 | }, 215 | enhancedModuleCreationStrategy = false, 216 | useKtsBuildFile = false, 217 | gradleFileFollowModule = false, 218 | packageName = testPackageName, 219 | addReadme = true, 220 | addGitIgnore = false, 221 | rootPathString = folder.root.toString(), 222 | previewMode = false 223 | 224 | ) 225 | 226 | // assert it was added to settings.gradle 227 | val settingsGradleFileContents = readFromFile(file = settingsGradleFile) 228 | assert( 229 | settingsGradleFileContents.contains("include(\":repository:database\")") 230 | ) 231 | 232 | // assert readme was generated 233 | assert( 234 | // root/repository/database/README.md 235 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + readmeFile).exists() 236 | ) 237 | 238 | // assert build.gradle is generated 239 | val buildGradleFile = File(folder.root.path + File.separator + modulePathAsFile + File.separator + buildGradleFileName) 240 | assert( 241 | // root/repository/database/build.gradle 242 | buildGradleFile.exists() 243 | ) 244 | 245 | // assert package name is included in build.gradle 246 | val buildGradleFileContents = readFromFile(buildGradleFile) 247 | assert( 248 | buildGradleFileContents.contains( 249 | " namespace = \"$testPackageName\"" 250 | ) 251 | ) 252 | 253 | // assert the correct package structure is generated 254 | assert( 255 | // root/repository/build.gradle 256 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "src/main/kotlin/com/joetr/test").exists() 257 | ) 258 | } 259 | 260 | @Test 261 | fun `android module created successfully with a kts build file`() { 262 | val modulePath = ":repository:database" 263 | val modulePathAsFile = "repository/database" 264 | 265 | fileWriter.createModule( 266 | settingsGradleFile = settingsGradleFile, 267 | workingDirectory = folder.root, 268 | modulePathAsString = modulePath, 269 | moduleType = ANDROID, 270 | showErrorDialog = { 271 | fail("No errors should be thrown") 272 | }, 273 | showSuccessDialog = { 274 | assert(true) 275 | }, 276 | enhancedModuleCreationStrategy = false, 277 | useKtsBuildFile = true, 278 | gradleFileFollowModule = false, 279 | packageName = testPackageName, 280 | addReadme = false, 281 | addGitIgnore = false, 282 | rootPathString = folder.root.toString(), 283 | previewMode = false 284 | 285 | ) 286 | 287 | // assert build.gradle.kts is generated 288 | val buildGradleFile = File(folder.root.path + File.separator + modulePathAsFile + File.separator + buildGradleKtsFileName) 289 | assert( 290 | // root/repository/database/build.gradle 291 | buildGradleFile.exists() 292 | ) 293 | } 294 | 295 | @Test 296 | fun `android module created successfully when include with no parenthesis`() { 297 | settingsGradleFile = folder.populateSettingsGradleWithFakeData() 298 | val modulePath = ":repository:database" 299 | 300 | fileWriter.createModule( 301 | settingsGradleFile = settingsGradleFile, 302 | workingDirectory = folder.root, 303 | modulePathAsString = modulePath, 304 | moduleType = ANDROID, 305 | showErrorDialog = { 306 | fail("No errors should be thrown") 307 | }, 308 | showSuccessDialog = { 309 | assert(true) 310 | }, 311 | enhancedModuleCreationStrategy = false, 312 | useKtsBuildFile = true, 313 | gradleFileFollowModule = false, 314 | packageName = testPackageName, 315 | addReadme = false, 316 | addGitIgnore = false, 317 | rootPathString = folder.root.toString(), 318 | previewMode = false 319 | 320 | ) 321 | 322 | // assert it was added to settings.gradle 323 | val settingsGradleFileContents = readFromFile(file = settingsGradleFile) 324 | assert( 325 | settingsGradleFileContents.contains("include(\":repository:database\")") 326 | ) 327 | } 328 | 329 | @Test 330 | fun `readme added to android module when setting is enabled`() { 331 | settingsGradleFile = folder.populateSettingsGradleWithFakeData() 332 | val modulePath = ":repository:database" 333 | val modulePathAsFile = "repository/database" 334 | 335 | fileWriter.createModule( 336 | settingsGradleFile = settingsGradleFile, 337 | workingDirectory = folder.root, 338 | modulePathAsString = modulePath, 339 | moduleType = ANDROID, 340 | showErrorDialog = { 341 | fail("No errors should be thrown") 342 | }, 343 | showSuccessDialog = { 344 | assert(true) 345 | }, 346 | enhancedModuleCreationStrategy = false, 347 | useKtsBuildFile = true, 348 | gradleFileFollowModule = false, 349 | packageName = testPackageName, 350 | addReadme = true, 351 | addGitIgnore = false, 352 | rootPathString = folder.root.toString(), 353 | previewMode = false 354 | 355 | ) 356 | 357 | // assert readme exists 358 | val buildGradleFile = File(folder.root.path + File.separator + modulePathAsFile + File.separator + "README.md") 359 | assert( 360 | buildGradleFile.exists() 361 | ) 362 | } 363 | 364 | @Test 365 | fun `readme is not added to android module when setting is disabled`() { 366 | settingsGradleFile = folder.populateSettingsGradleWithFakeData() 367 | val modulePath = ":repository:database" 368 | val modulePathAsFile = "repository/database" 369 | 370 | fileWriter.createModule( 371 | settingsGradleFile = settingsGradleFile, 372 | workingDirectory = folder.root, 373 | modulePathAsString = modulePath, 374 | moduleType = ANDROID, 375 | showErrorDialog = { 376 | fail("No errors should be thrown") 377 | }, 378 | showSuccessDialog = { 379 | assert(true) 380 | }, 381 | enhancedModuleCreationStrategy = false, 382 | useKtsBuildFile = true, 383 | gradleFileFollowModule = false, 384 | packageName = testPackageName, 385 | addReadme = false, 386 | addGitIgnore = false, 387 | rootPathString = folder.root.toString(), 388 | previewMode = false 389 | 390 | ) 391 | 392 | // assert readme does not exists 393 | val buildGradleFile = File(folder.root.path + File.separator + modulePathAsFile + File.separator + "README.md") 394 | assert( 395 | buildGradleFile.exists().not() 396 | ) 397 | } 398 | 399 | @Test 400 | fun `gitignore is not generated in android module when setting is disabled`() { 401 | val modulePath = ":repository" 402 | val modulePathAsFile = "repository" 403 | 404 | fileWriter.createModule( 405 | settingsGradleFile = settingsGradleFile, 406 | workingDirectory = folder.root, 407 | modulePathAsString = modulePath, 408 | moduleType = ANDROID, 409 | showErrorDialog = { 410 | fail("No errors should be thrown") 411 | }, 412 | showSuccessDialog = { 413 | assert(true) 414 | }, 415 | enhancedModuleCreationStrategy = false, 416 | useKtsBuildFile = false, 417 | gradleFileFollowModule = false, 418 | packageName = testPackageName, 419 | addReadme = false, 420 | addGitIgnore = false, 421 | rootPathString = folder.root.toString(), 422 | previewMode = false 423 | 424 | ) 425 | 426 | // assert gitignore was not generated 427 | assert( 428 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + File.separator + ".gitignore").exists() 429 | .not() 430 | ) 431 | } 432 | 433 | @Test 434 | fun `gitignore is generated in android module with default settings when setting is enabled`() { 435 | val modulePath = ":repository" 436 | val modulePathAsFile = "repository" 437 | 438 | fileWriter.createModule( 439 | settingsGradleFile = settingsGradleFile, 440 | workingDirectory = folder.root, 441 | modulePathAsString = modulePath, 442 | moduleType = ANDROID, 443 | showErrorDialog = { 444 | fail("No errors should be thrown") 445 | }, 446 | showSuccessDialog = { 447 | assert(true) 448 | }, 449 | enhancedModuleCreationStrategy = false, 450 | useKtsBuildFile = false, 451 | gradleFileFollowModule = false, 452 | packageName = testPackageName, 453 | addReadme = false, 454 | addGitIgnore = true, 455 | rootPathString = folder.root.toString(), 456 | previewMode = false 457 | 458 | ) 459 | 460 | // assert gitignore was generated and has the expected contents 461 | val gitignoreFile = File(folder.root.path + File.separator + modulePathAsFile + File.separator + File.separator + ".gitignore") 462 | val gitignoreFileContents = readFromFile(file = gitignoreFile) 463 | assertEquals( 464 | GitIgnoreTemplate.data, 465 | gitignoreFileContents.joinToString("\n") 466 | ) 467 | } 468 | 469 | @Test 470 | fun `gitignore is generated in android module with custom settings when setting is enabled`() { 471 | val modulePath = ":repository" 472 | val modulePathAsFile = "repository" 473 | 474 | val template = """ 475 | this is a custom template 476 | """.trimIndent() 477 | 478 | fakePreferenceService.preferenceState.gitignoreTemplate = template 479 | 480 | fileWriter.createModule( 481 | settingsGradleFile = settingsGradleFile, 482 | workingDirectory = folder.root, 483 | modulePathAsString = modulePath, 484 | moduleType = ANDROID, 485 | showErrorDialog = { 486 | fail("No errors should be thrown") 487 | }, 488 | showSuccessDialog = { 489 | assert(true) 490 | }, 491 | enhancedModuleCreationStrategy = false, 492 | useKtsBuildFile = false, 493 | gradleFileFollowModule = false, 494 | packageName = testPackageName, 495 | addReadme = false, 496 | addGitIgnore = true, 497 | rootPathString = folder.root.toString(), 498 | previewMode = false 499 | 500 | ) 501 | 502 | // assert gitignore was generated and has the expected contents 503 | val gitignoreFile = File(folder.root.path + File.separator + modulePathAsFile + File.separator + File.separator + ".gitignore") 504 | val gitignoreFileContents = readFromFile(file = gitignoreFile) 505 | assertEquals( 506 | template, 507 | gitignoreFileContents.joinToString("\n") 508 | ) 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /src/test/kotlin/com/joetr/modulemaker/EnhancedModuleMakerTest.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker 2 | 3 | import com.joetr.modulemaker.file.FileWriter 4 | import com.joetr.modulemaker.persistence.PreferenceService 5 | import com.joetr.modulemaker.persistence.PreferenceServiceImpl 6 | import com.joetr.modulemaker.template.GitIgnoreTemplate 7 | import org.junit.Assert 8 | import org.junit.Assert.fail 9 | import org.junit.Before 10 | import org.junit.Rule 11 | import org.junit.Test 12 | import org.junit.rules.TemporaryFolder 13 | import java.io.File 14 | 15 | class EnhancedModuleMakerTest { 16 | 17 | @JvmField 18 | @Rule 19 | var folder = TemporaryFolder() 20 | 21 | var testState = PreferenceServiceImpl.Companion.State() 22 | 23 | private val fakePreferenceService = object : PreferenceService { 24 | override var preferenceState: PreferenceServiceImpl.Companion.State 25 | get() = testState 26 | set(value) { 27 | testState = value 28 | } 29 | } 30 | 31 | private val fileWriter = FileWriter( 32 | preferenceService = fakePreferenceService 33 | ) 34 | 35 | private lateinit var settingsGradleFile: File 36 | 37 | @Before 38 | fun before() { 39 | settingsGradleFile = folder.populateSettingsGradleKtsWithFakeData() 40 | } 41 | 42 | @Test 43 | fun `enhanced module created successfully`() { 44 | val modulePath = ":repository" 45 | val modulePathAsFile = "repository" 46 | 47 | fileWriter.createModule( 48 | settingsGradleFile = settingsGradleFile, 49 | workingDirectory = folder.root, 50 | modulePathAsString = modulePath, 51 | moduleType = ANDROID, 52 | showErrorDialog = { 53 | fail("No errors should be thrown") 54 | }, 55 | showSuccessDialog = { 56 | assert(true) 57 | }, 58 | enhancedModuleCreationStrategy = true, 59 | useKtsBuildFile = false, 60 | gradleFileFollowModule = false, 61 | packageName = testPackageName, 62 | addReadme = true, 63 | addGitIgnore = false, 64 | rootPathString = folder.root.toString(), 65 | previewMode = false 66 | 67 | ) 68 | 69 | // assert it was added to settings.gradle 70 | val settingsGradleFileContents = readFromFile(file = settingsGradleFile) 71 | assert( 72 | settingsGradleFileContents.contains("include(\":repository:api\")") 73 | ) 74 | assert( 75 | settingsGradleFileContents.contains("include(\":repository:glue\")") 76 | ) 77 | assert( 78 | settingsGradleFileContents.contains("include(\":repository:impl\")") 79 | ) 80 | 81 | // assert readme was generated in the api module 82 | assert( 83 | // root/repository/api/README.md 84 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "api" + File.separator + readmeFile).exists() 85 | ) 86 | 87 | // assert build.gradle is generated for all 3 modules 88 | val buildGradleFileApi = 89 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "api" + File.separator + buildGradleFileName) 90 | val buildGradleFileGlue = 91 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "glue" + File.separator + buildGradleFileName) 92 | val buildGradleFileImpl = 93 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "impl" + File.separator + buildGradleFileName) 94 | assert(buildGradleFileApi.exists()) 95 | assert(buildGradleFileGlue.exists()) 96 | assert(buildGradleFileImpl.exists()) 97 | 98 | // assert package name is included in build.gradle 99 | val buildGradleApiFileContents = readFromFile(buildGradleFileApi) 100 | val buildGradleGlueFileContents = readFromFile(buildGradleFileGlue) 101 | val buildGradleImplFileContents = readFromFile(buildGradleFileImpl) 102 | assert( 103 | buildGradleApiFileContents.contains( 104 | " namespace = \"$testPackageName.api\"" 105 | ) 106 | ) 107 | assert( 108 | buildGradleGlueFileContents.contains( 109 | " namespace = \"$testPackageName.glue\"" 110 | ) 111 | ) 112 | assert( 113 | buildGradleImplFileContents.contains( 114 | " namespace = \"$testPackageName.impl\"" 115 | ) 116 | ) 117 | 118 | // assert the correct package structure is generated 119 | assert( 120 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "api/src/main/kotlin/com/joetr/test/api").exists() 121 | ) 122 | assert( 123 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "glue/src/main/kotlin/com/joetr/test/glue").exists() 124 | ) 125 | assert( 126 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "impl/src/main/kotlin/com/joetr/test/impl").exists() 127 | ) 128 | } 129 | 130 | @Test 131 | fun `when a template is set, the package name variable is replaced for each module created`() { 132 | val modulePath = ":repository" 133 | val modulePathAsFile = "repository" 134 | 135 | val template = """ 136 | this is a custom template 137 | 138 | android { 139 | namespace = "${'$'}{packageName}" 140 | } 141 | """.trimIndent() 142 | 143 | fakePreferenceService.preferenceState.apiTemplate = template 144 | fakePreferenceService.preferenceState.glueTemplate = template 145 | fakePreferenceService.preferenceState.implTemplate = template 146 | 147 | fileWriter.createModule( 148 | settingsGradleFile = settingsGradleFile, 149 | workingDirectory = folder.root, 150 | modulePathAsString = modulePath, 151 | moduleType = ANDROID, 152 | showErrorDialog = { 153 | fail("No errors should be thrown") 154 | }, 155 | showSuccessDialog = { 156 | assert(true) 157 | }, 158 | enhancedModuleCreationStrategy = true, 159 | useKtsBuildFile = false, 160 | gradleFileFollowModule = false, 161 | packageName = testPackageName, 162 | addReadme = false, 163 | addGitIgnore = false, 164 | rootPathString = folder.root.toString(), 165 | previewMode = false 166 | 167 | ) 168 | 169 | // assert build.gradle is generated for all 3 modules 170 | val buildGradleFileApi = 171 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "api" + File.separator + buildGradleFileName) 172 | val buildGradleFileGlue = 173 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "glue" + File.separator + buildGradleFileName) 174 | val buildGradleFileImpl = 175 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "impl" + File.separator + buildGradleFileName) 176 | assert(buildGradleFileApi.exists()) 177 | assert(buildGradleFileGlue.exists()) 178 | assert(buildGradleFileImpl.exists()) 179 | 180 | // assert package name is included in build.gradle 181 | val buildGradleApiFileContents = readFromFile(buildGradleFileApi) 182 | val buildGradleGlueFileContents = readFromFile(buildGradleFileGlue) 183 | val buildGradleImplFileContents = readFromFile(buildGradleFileImpl) 184 | assert( 185 | buildGradleApiFileContents.contains( 186 | " namespace = \"$testPackageName.api\"" 187 | ) 188 | ) 189 | assert( 190 | buildGradleGlueFileContents.contains( 191 | " namespace = \"$testPackageName.glue\"" 192 | ) 193 | ) 194 | assert( 195 | buildGradleImplFileContents.contains( 196 | " namespace = \"$testPackageName.impl\"" 197 | ) 198 | ) 199 | } 200 | 201 | @Test 202 | fun `when a template is set, that is used instead of default for creating build gradle`() { 203 | val modulePath = ":repository:database" 204 | val modulePathAsFile = "repository/database" 205 | 206 | val template = "test template" 207 | 208 | fakePreferenceService.preferenceState.apiTemplate = template 209 | fakePreferenceService.preferenceState.glueTemplate = template 210 | fakePreferenceService.preferenceState.implTemplate = template 211 | 212 | fileWriter.createModule( 213 | settingsGradleFile = settingsGradleFile, 214 | workingDirectory = folder.root, 215 | modulePathAsString = modulePath, 216 | moduleType = ANDROID, 217 | showErrorDialog = { 218 | fail("No errors should be thrown") 219 | }, 220 | showSuccessDialog = { 221 | assert(true) 222 | }, 223 | enhancedModuleCreationStrategy = true, 224 | useKtsBuildFile = false, 225 | gradleFileFollowModule = false, 226 | packageName = testPackageName, 227 | addReadme = false, 228 | addGitIgnore = false, 229 | rootPathString = folder.root.toString(), 230 | previewMode = false 231 | 232 | ) 233 | 234 | // assert build.gradle is generated for all 3 modules 235 | val buildGradleFileApi = 236 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "api" + File.separator + buildGradleFileName) 237 | val buildGradleFileGlue = 238 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "glue" + File.separator + buildGradleFileName) 239 | val buildGradleFileImpl = 240 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "impl" + File.separator + buildGradleFileName) 241 | assert(buildGradleFileApi.exists()) 242 | assert(buildGradleFileGlue.exists()) 243 | assert(buildGradleFileImpl.exists()) 244 | 245 | // assert package name is included in build.gradle 246 | val buildGradleApiFileContents = readFromFile(buildGradleFileApi) 247 | val buildGradleGlueFileContents = readFromFile(buildGradleFileGlue) 248 | val buildGradleImplFileContents = readFromFile(buildGradleFileImpl) 249 | assert( 250 | buildGradleApiFileContents.contains( 251 | template 252 | ) 253 | ) 254 | assert( 255 | buildGradleGlueFileContents.contains( 256 | template 257 | ) 258 | ) 259 | assert( 260 | buildGradleImplFileContents.contains( 261 | template 262 | ) 263 | ) 264 | } 265 | 266 | @Test 267 | fun `readme is not generated in enhanced module when setting is disabled`() { 268 | val modulePath = ":repository" 269 | val modulePathAsFile = "repository" 270 | 271 | fileWriter.createModule( 272 | settingsGradleFile = settingsGradleFile, 273 | workingDirectory = folder.root, 274 | modulePathAsString = modulePath, 275 | moduleType = ANDROID, 276 | showErrorDialog = { 277 | fail("No errors should be thrown") 278 | }, 279 | showSuccessDialog = { 280 | assert(true) 281 | }, 282 | enhancedModuleCreationStrategy = true, 283 | useKtsBuildFile = false, 284 | gradleFileFollowModule = false, 285 | packageName = testPackageName, 286 | addReadme = false, 287 | addGitIgnore = false, 288 | rootPathString = folder.root.toString(), 289 | previewMode = false 290 | 291 | ) 292 | 293 | // assert readme was not generated in the api module 294 | assert( 295 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "api" + File.separator + readmeFile).exists() 296 | .not() 297 | ) 298 | } 299 | 300 | @Test 301 | fun `gitignore is not generated in enhanced module when setting is disabled`() { 302 | val modulePath = ":repository" 303 | val modulePathAsFile = "repository" 304 | 305 | fileWriter.createModule( 306 | settingsGradleFile = settingsGradleFile, 307 | workingDirectory = folder.root, 308 | modulePathAsString = modulePath, 309 | moduleType = ANDROID, 310 | showErrorDialog = { 311 | fail("No errors should be thrown") 312 | }, 313 | showSuccessDialog = { 314 | assert(true) 315 | }, 316 | enhancedModuleCreationStrategy = true, 317 | useKtsBuildFile = false, 318 | gradleFileFollowModule = false, 319 | packageName = testPackageName, 320 | addReadme = false, 321 | addGitIgnore = false, 322 | rootPathString = folder.root.toString(), 323 | previewMode = false 324 | 325 | ) 326 | 327 | // assert gitignore was not generated in any of the modules module 328 | assert( 329 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "api" + File.separator + ".gitignore").exists() 330 | .not() 331 | ) 332 | assert( 333 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "impl" + File.separator + ".gitignore").exists() 334 | .not() 335 | ) 336 | assert( 337 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "glue" + File.separator + ".gitignore").exists() 338 | .not() 339 | ) 340 | } 341 | 342 | @Test 343 | fun `gitignore is generated in enhanced module with default settings when setting is enabled`() { 344 | val modulePath = ":repository" 345 | val modulePathAsFile = "repository" 346 | 347 | fileWriter.createModule( 348 | settingsGradleFile = settingsGradleFile, 349 | workingDirectory = folder.root, 350 | modulePathAsString = modulePath, 351 | moduleType = ANDROID, 352 | showErrorDialog = { 353 | fail("No errors should be thrown") 354 | }, 355 | showSuccessDialog = { 356 | assert(true) 357 | }, 358 | enhancedModuleCreationStrategy = true, 359 | useKtsBuildFile = false, 360 | gradleFileFollowModule = false, 361 | packageName = testPackageName, 362 | addReadme = false, 363 | addGitIgnore = true, 364 | rootPathString = folder.root.toString(), 365 | previewMode = false 366 | 367 | ) 368 | 369 | val apiGitIgnore = File(folder.root.path + File.separator + modulePathAsFile + File.separator + "api" + File.separator + ".gitignore") 370 | val apiGitignoreFileContents = readFromFile(file = apiGitIgnore) 371 | val glueGitIgnore = File(folder.root.path + File.separator + modulePathAsFile + File.separator + "glue" + File.separator + ".gitignore") 372 | val glueGitignoreFileContents = readFromFile(file = glueGitIgnore) 373 | val implGitIgnore = File(folder.root.path + File.separator + modulePathAsFile + File.separator + "impl" + File.separator + ".gitignore") 374 | val implGitignoreFileContents = readFromFile(file = implGitIgnore) 375 | 376 | Assert.assertEquals( 377 | GitIgnoreTemplate.data, 378 | apiGitignoreFileContents.joinToString("\n") 379 | ) 380 | 381 | Assert.assertEquals( 382 | GitIgnoreTemplate.data, 383 | glueGitignoreFileContents.joinToString("\n") 384 | ) 385 | 386 | Assert.assertEquals( 387 | GitIgnoreTemplate.data, 388 | implGitignoreFileContents.joinToString("\n") 389 | ) 390 | } 391 | 392 | @Test 393 | fun `gitignore is generated in android module with custom settings when setting is enabled`() { 394 | val modulePath = ":repository" 395 | val modulePathAsFile = "repository" 396 | 397 | val template = """ 398 | this is a custom template 399 | """.trimIndent() 400 | 401 | fakePreferenceService.preferenceState.gitignoreTemplate = template 402 | 403 | fileWriter.createModule( 404 | settingsGradleFile = settingsGradleFile, 405 | workingDirectory = folder.root, 406 | modulePathAsString = modulePath, 407 | moduleType = ANDROID, 408 | showErrorDialog = { 409 | fail("No errors should be thrown") 410 | }, 411 | showSuccessDialog = { 412 | assert(true) 413 | }, 414 | enhancedModuleCreationStrategy = true, 415 | useKtsBuildFile = false, 416 | gradleFileFollowModule = false, 417 | packageName = testPackageName, 418 | addReadme = false, 419 | addGitIgnore = true, 420 | rootPathString = folder.root.toString(), 421 | previewMode = false 422 | 423 | ) 424 | 425 | val apiGitIgnore = File(folder.root.path + File.separator + modulePathAsFile + File.separator + "api" + File.separator + ".gitignore") 426 | val apiGitignoreFileContents = readFromFile(file = apiGitIgnore) 427 | val glueGitIgnore = File(folder.root.path + File.separator + modulePathAsFile + File.separator + "glue" + File.separator + ".gitignore") 428 | val glueGitignoreFileContents = readFromFile(file = glueGitIgnore) 429 | val implGitIgnore = File(folder.root.path + File.separator + modulePathAsFile + File.separator + "impl" + File.separator + ".gitignore") 430 | val implGitignoreFileContents = readFromFile(file = implGitIgnore) 431 | 432 | Assert.assertEquals( 433 | template, 434 | apiGitignoreFileContents.joinToString("\n") 435 | ) 436 | 437 | Assert.assertEquals( 438 | template, 439 | glueGitignoreFileContents.joinToString("\n") 440 | ) 441 | 442 | Assert.assertEquals( 443 | template, 444 | implGitignoreFileContents.joinToString("\n") 445 | ) 446 | } 447 | 448 | @Test 449 | fun `create module works with 2 parameters`() { 450 | settingsGradleFile.delete() 451 | settingsGradleFile = folder.populateSettingsGradleKtsWithFakeFilePathData() 452 | val modulePath = ":repository:network" 453 | val modulePathAsFile = "repository/network" 454 | val rootPathString = folder.root.toString().removePrefix("/") 455 | 456 | fileWriter.createModule( 457 | settingsGradleFile = settingsGradleFile, 458 | workingDirectory = folder.root, 459 | modulePathAsString = modulePath, 460 | moduleType = KOTLIN, 461 | showErrorDialog = { 462 | fail("No errors should be thrown") 463 | }, 464 | showSuccessDialog = { 465 | assert(true) 466 | }, 467 | enhancedModuleCreationStrategy = true, 468 | useKtsBuildFile = false, 469 | gradleFileFollowModule = false, 470 | packageName = testPackageName, 471 | addReadme = false, 472 | addGitIgnore = true, 473 | rootPathString = folder.root.toString(), 474 | previewMode = false 475 | 476 | ) 477 | 478 | val settingsGradleFileContents = readFromFile(file = settingsGradleFile) 479 | Assert.assertEquals( 480 | "include(\"$modulePath:api\", \"$rootPathString/$modulePathAsFile/api\")", 481 | settingsGradleFileContents[56] 482 | ) 483 | Assert.assertEquals( 484 | "include(\"$modulePath:impl\", \"$rootPathString/$modulePathAsFile/impl\")", 485 | settingsGradleFileContents[57] 486 | ) 487 | Assert.assertEquals( 488 | "include(\"$modulePath:glue\", \"$rootPathString/$modulePathAsFile/glue\")", 489 | settingsGradleFileContents[58] 490 | ) 491 | } 492 | 493 | @Test 494 | fun `custom module names used when set`() { 495 | fakePreferenceService.preferenceState.glueModuleName = "customglue" 496 | fakePreferenceService.preferenceState.apiModuleName = "customapi" 497 | fakePreferenceService.preferenceState.implModuleName = "customimpl" 498 | 499 | val modulePath = ":repository" 500 | val modulePathAsFile = "repository" 501 | 502 | fileWriter.createModule( 503 | settingsGradleFile = settingsGradleFile, 504 | workingDirectory = folder.root, 505 | modulePathAsString = modulePath, 506 | moduleType = ANDROID, 507 | showErrorDialog = { 508 | fail("No errors should be thrown") 509 | }, 510 | showSuccessDialog = { 511 | assert(true) 512 | }, 513 | enhancedModuleCreationStrategy = true, 514 | useKtsBuildFile = false, 515 | gradleFileFollowModule = false, 516 | packageName = testPackageName, 517 | addReadme = true, 518 | addGitIgnore = false, 519 | rootPathString = folder.root.toString(), 520 | previewMode = false 521 | 522 | ) 523 | 524 | // assert it was added to settings.gradle 525 | val settingsGradleFileContents = readFromFile(file = settingsGradleFile) 526 | assert( 527 | settingsGradleFileContents.contains("include(\":repository:customapi\")") 528 | ) 529 | assert( 530 | settingsGradleFileContents.contains("include(\":repository:customglue\")") 531 | ) 532 | assert( 533 | settingsGradleFileContents.contains("include(\":repository:customimpl\")") 534 | ) 535 | 536 | // assert readme was generated in the api module 537 | assert( 538 | // root/repository/api/README.md 539 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "customapi" + File.separator + readmeFile).exists() 540 | ) 541 | 542 | // assert build.gradle is generated for all 3 modules 543 | val buildGradleFileApi = 544 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "customapi" + File.separator + buildGradleFileName) 545 | val buildGradleFileGlue = 546 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "customglue" + File.separator + buildGradleFileName) 547 | val buildGradleFileImpl = 548 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "customimpl" + File.separator + buildGradleFileName) 549 | assert(buildGradleFileApi.exists()) 550 | assert(buildGradleFileGlue.exists()) 551 | assert(buildGradleFileImpl.exists()) 552 | 553 | // assert package name is included in build.gradle 554 | val buildGradleApiFileContents = readFromFile(buildGradleFileApi) 555 | val buildGradleGlueFileContents = readFromFile(buildGradleFileGlue) 556 | val buildGradleImplFileContents = readFromFile(buildGradleFileImpl) 557 | assert( 558 | buildGradleApiFileContents.contains( 559 | " namespace = \"$testPackageName.customapi\"" 560 | ) 561 | ) 562 | assert( 563 | buildGradleGlueFileContents.contains( 564 | " namespace = \"$testPackageName.customglue\"" 565 | ) 566 | ) 567 | assert( 568 | buildGradleImplFileContents.contains( 569 | " namespace = \"$testPackageName.customimpl\"" 570 | ) 571 | ) 572 | 573 | // assert the correct package structure is generated 574 | assert( 575 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "customapi/src/main/kotlin/com/joetr/test/customapi").exists() 576 | ) 577 | assert( 578 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "customglue/src/main/kotlin/com/joetr/test/customglue").exists() 579 | ) 580 | assert( 581 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "customimpl/src/main/kotlin/com/joetr/test/customimpl").exists() 582 | ) 583 | } 584 | } 585 | -------------------------------------------------------------------------------- /src/test/kotlin/com/joetr/modulemaker/KotlinModuleMakerTest.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker 2 | 3 | import com.joetr.modulemaker.file.FileWriter 4 | import com.joetr.modulemaker.persistence.PreferenceService 5 | import com.joetr.modulemaker.persistence.PreferenceServiceImpl 6 | import com.joetr.modulemaker.template.GitIgnoreTemplate 7 | import org.junit.Assert 8 | import org.junit.Assert.assertEquals 9 | import org.junit.Before 10 | import org.junit.Rule 11 | import org.junit.Test 12 | import org.junit.rules.TemporaryFolder 13 | import java.io.File 14 | 15 | class KotlinModuleMakerTest { 16 | 17 | @JvmField 18 | @Rule 19 | var folder = TemporaryFolder() 20 | 21 | var testState = PreferenceServiceImpl.Companion.State() 22 | 23 | private val fakePreferenceService = object : PreferenceService { 24 | override var preferenceState: PreferenceServiceImpl.Companion.State 25 | get() = testState 26 | set(value) { 27 | testState = value 28 | } 29 | } 30 | 31 | private val fileWriter = FileWriter( 32 | preferenceService = fakePreferenceService 33 | ) 34 | 35 | private lateinit var settingsGradleFile: File 36 | 37 | @Before 38 | fun before() { 39 | settingsGradleFile = folder.populateSettingsGradleKtsWithFakeData() 40 | } 41 | 42 | @Test 43 | fun `kotlin module created successfully`() { 44 | val modulePath = ":repository" 45 | val modulePathAsFile = "repository" 46 | 47 | fileWriter.createModule( 48 | settingsGradleFile = settingsGradleFile, 49 | workingDirectory = folder.root, 50 | modulePathAsString = modulePath, 51 | moduleType = KOTLIN, 52 | showErrorDialog = { 53 | Assert.fail("No errors should be thrown") 54 | }, 55 | showSuccessDialog = { 56 | assert(true) 57 | }, 58 | enhancedModuleCreationStrategy = false, 59 | useKtsBuildFile = false, 60 | gradleFileFollowModule = false, 61 | packageName = testPackageName, 62 | addReadme = true, 63 | addGitIgnore = false, 64 | rootPathString = folder.root.toString(), 65 | previewMode = false 66 | 67 | ) 68 | 69 | // assert it was added to settings.gradle 70 | val settingsGradleFileContents = readFromFile(file = settingsGradleFile) 71 | assert( 72 | settingsGradleFileContents.contains("include(\":repository\")") 73 | ) 74 | 75 | // assert readme was generated 76 | assert( 77 | // root/repository/README.md 78 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + readmeFile).exists() 79 | ) 80 | 81 | // assert build.gradle is generated 82 | assert( 83 | // root/repository/build.gradle 84 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + buildGradleFileName).exists() 85 | ) 86 | 87 | // assert the correct package structure is generated 88 | assert( 89 | // root/repository/build.gradle 90 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "src/main/kotlin/com/joetr/test").exists() 91 | ) 92 | } 93 | 94 | @Test 95 | fun `when a template is set, that is used instead of default for creating build gradle`() { 96 | val modulePath = ":repository" 97 | val modulePathAsFile = "repository" 98 | val template = "test template" 99 | fakePreferenceService.preferenceState.kotlinTemplate = template 100 | 101 | fileWriter.createModule( 102 | settingsGradleFile = settingsGradleFile, 103 | workingDirectory = folder.root, 104 | modulePathAsString = modulePath, 105 | moduleType = KOTLIN, 106 | showErrorDialog = { 107 | Assert.fail("No errors should be thrown") 108 | }, 109 | showSuccessDialog = { 110 | assert(true) 111 | }, 112 | enhancedModuleCreationStrategy = false, 113 | useKtsBuildFile = false, 114 | gradleFileFollowModule = false, 115 | packageName = testPackageName, 116 | addReadme = false, 117 | addGitIgnore = false, 118 | rootPathString = folder.root.toString(), 119 | previewMode = false 120 | 121 | ) 122 | 123 | // assert build.gradle is generated 124 | val buildGradleFile = 125 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + buildGradleFileName) 126 | assert( 127 | // root/repository/build.gradle 128 | buildGradleFile.exists() 129 | ) 130 | 131 | // assert package name is included in build.gradle 132 | val buildGradleFileContents = readFromFile(buildGradleFile) 133 | assert( 134 | buildGradleFileContents.contains( 135 | template 136 | ) 137 | ) 138 | } 139 | 140 | @Test 141 | fun `kotlin module created successfully with a kts build file named after module`() { 142 | val modulePath = ":repository:database" 143 | val modulePathAsFile = "repository/database" 144 | 145 | fileWriter.createModule( 146 | settingsGradleFile = settingsGradleFile, 147 | workingDirectory = folder.root, 148 | modulePathAsString = modulePath, 149 | moduleType = KOTLIN, 150 | showErrorDialog = { 151 | Assert.fail("No errors should be thrown") 152 | }, 153 | showSuccessDialog = { 154 | assert(true) 155 | }, 156 | enhancedModuleCreationStrategy = false, 157 | useKtsBuildFile = true, 158 | gradleFileFollowModule = true, 159 | packageName = testPackageName, 160 | addReadme = false, 161 | addGitIgnore = false, 162 | rootPathString = folder.root.toString(), 163 | previewMode = false 164 | 165 | ) 166 | 167 | // assert build.gradle.kts is generated 168 | val buildGradleFile = 169 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "database.gradle.kts") 170 | assert( 171 | // root/repository/database/build.gradle 172 | buildGradleFile.exists() 173 | ) 174 | } 175 | 176 | @Test 177 | fun `kotlin module created successfully with a gradle build file named after module`() { 178 | val modulePath = ":repository:database" 179 | val modulePathAsFile = "repository/database" 180 | 181 | fileWriter.createModule( 182 | settingsGradleFile = settingsGradleFile, 183 | workingDirectory = folder.root, 184 | modulePathAsString = modulePath, 185 | moduleType = KOTLIN, 186 | showErrorDialog = { 187 | Assert.fail("No errors should be thrown") 188 | }, 189 | showSuccessDialog = { 190 | assert(true) 191 | }, 192 | enhancedModuleCreationStrategy = false, 193 | useKtsBuildFile = false, 194 | gradleFileFollowModule = true, 195 | packageName = testPackageName, 196 | addReadme = false, 197 | addGitIgnore = false, 198 | rootPathString = folder.root.toString(), 199 | previewMode = false 200 | 201 | ) 202 | 203 | // assert build.gradle.kts is generated 204 | val buildGradleFile = 205 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "database.gradle") 206 | assert( 207 | // root/repository/database/build.gradle 208 | buildGradleFile.exists() 209 | ) 210 | } 211 | 212 | @Test 213 | fun `readme is not added to kotlin module when setting is disabled`() { 214 | val modulePath = ":repository:database" 215 | val modulePathAsFile = "repository/database" 216 | 217 | fileWriter.createModule( 218 | settingsGradleFile = settingsGradleFile, 219 | workingDirectory = folder.root, 220 | modulePathAsString = modulePath, 221 | moduleType = KOTLIN, 222 | showErrorDialog = { 223 | Assert.fail("No errors should be thrown") 224 | }, 225 | showSuccessDialog = { 226 | assert(true) 227 | }, 228 | enhancedModuleCreationStrategy = false, 229 | useKtsBuildFile = false, 230 | gradleFileFollowModule = true, 231 | packageName = testPackageName, 232 | addReadme = false, 233 | addGitIgnore = false, 234 | rootPathString = folder.root.toString(), 235 | previewMode = false 236 | 237 | ) 238 | 239 | // assert readme is NOT generated 240 | val buildGradleFile = 241 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "README.md") 242 | assert( 243 | buildGradleFile.exists().not() 244 | ) 245 | } 246 | 247 | @Test 248 | fun `readme is added to kotlin module when setting is enabled`() { 249 | val modulePath = ":repository:database" 250 | val modulePathAsFile = "repository/database" 251 | 252 | fileWriter.createModule( 253 | settingsGradleFile = settingsGradleFile, 254 | workingDirectory = folder.root, 255 | modulePathAsString = modulePath, 256 | moduleType = KOTLIN, 257 | showErrorDialog = { 258 | Assert.fail("No errors should be thrown") 259 | }, 260 | showSuccessDialog = { 261 | assert(true) 262 | }, 263 | enhancedModuleCreationStrategy = false, 264 | useKtsBuildFile = false, 265 | gradleFileFollowModule = true, 266 | packageName = testPackageName, 267 | addReadme = true, 268 | addGitIgnore = false, 269 | rootPathString = folder.root.toString(), 270 | previewMode = false 271 | 272 | ) 273 | 274 | // assert readme is generated 275 | val buildGradleFile = 276 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + "README.md") 277 | assert( 278 | buildGradleFile.exists() 279 | ) 280 | } 281 | 282 | @Test 283 | fun `gitignore is not generated in kotlin module when setting is disabled`() { 284 | val modulePath = ":repository" 285 | val modulePathAsFile = "repository" 286 | 287 | fileWriter.createModule( 288 | settingsGradleFile = settingsGradleFile, 289 | workingDirectory = folder.root, 290 | modulePathAsString = modulePath, 291 | moduleType = KOTLIN, 292 | showErrorDialog = { 293 | Assert.fail("No errors should be thrown") 294 | }, 295 | showSuccessDialog = { 296 | assert(true) 297 | }, 298 | enhancedModuleCreationStrategy = false, 299 | useKtsBuildFile = false, 300 | gradleFileFollowModule = false, 301 | packageName = testPackageName, 302 | addReadme = false, 303 | addGitIgnore = false, 304 | rootPathString = folder.root.toString(), 305 | previewMode = false 306 | 307 | ) 308 | 309 | // assert gitignore was not generated 310 | assert( 311 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + File.separator + ".gitignore").exists() 312 | .not() 313 | ) 314 | } 315 | 316 | @Test 317 | fun `gitignore is generated in kotlin module with default settings when setting is enabled`() { 318 | val modulePath = ":repository" 319 | val modulePathAsFile = "repository" 320 | 321 | fileWriter.createModule( 322 | settingsGradleFile = settingsGradleFile, 323 | workingDirectory = folder.root, 324 | modulePathAsString = modulePath, 325 | moduleType = KOTLIN, 326 | showErrorDialog = { 327 | Assert.fail("No errors should be thrown") 328 | }, 329 | showSuccessDialog = { 330 | assert(true) 331 | }, 332 | enhancedModuleCreationStrategy = false, 333 | useKtsBuildFile = false, 334 | gradleFileFollowModule = false, 335 | packageName = testPackageName, 336 | addReadme = false, 337 | addGitIgnore = true, 338 | rootPathString = folder.root.toString(), 339 | previewMode = false 340 | 341 | ) 342 | 343 | // assert gitignore was generated and has the expected contents 344 | val gitignoreFile = 345 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + File.separator + ".gitignore") 346 | val gitignoreFileContents = readFromFile(file = gitignoreFile) 347 | assertEquals( 348 | GitIgnoreTemplate.data, 349 | gitignoreFileContents.joinToString("\n") 350 | ) 351 | } 352 | 353 | @Test 354 | fun `gitignore is generated in kotlin module with custom settings when setting is enabled`() { 355 | val modulePath = ":repository" 356 | val modulePathAsFile = "repository" 357 | 358 | val template = """ 359 | this is a custom template 360 | """.trimIndent() 361 | 362 | fakePreferenceService.preferenceState.gitignoreTemplate = template 363 | 364 | fileWriter.createModule( 365 | settingsGradleFile = settingsGradleFile, 366 | workingDirectory = folder.root, 367 | modulePathAsString = modulePath, 368 | moduleType = KOTLIN, 369 | showErrorDialog = { 370 | Assert.fail("No errors should be thrown") 371 | }, 372 | showSuccessDialog = { 373 | assert(true) 374 | }, 375 | enhancedModuleCreationStrategy = false, 376 | useKtsBuildFile = false, 377 | gradleFileFollowModule = false, 378 | packageName = testPackageName, 379 | addReadme = false, 380 | addGitIgnore = true, 381 | rootPathString = folder.root.toString(), 382 | previewMode = false 383 | 384 | ) 385 | 386 | // assert gitignore was generated and has the expected contents 387 | val gitignoreFile = 388 | File(folder.root.path + File.separator + modulePathAsFile + File.separator + File.separator + ".gitignore") 389 | val gitignoreFileContents = readFromFile(file = gitignoreFile) 390 | assertEquals( 391 | template, 392 | gitignoreFileContents.joinToString("\n") 393 | ) 394 | } 395 | 396 | @Test 397 | fun `create module works with 2 parameters`() { 398 | settingsGradleFile.delete() 399 | settingsGradleFile = folder.populateSettingsGradleKtsWithFakeFilePathData() 400 | val modulePath = ":repository:network" 401 | val modulePathAsFile = "repository/network" 402 | val rootPathString = folder.root.toString().removePrefix("/") 403 | 404 | fileWriter.createModule( 405 | settingsGradleFile = settingsGradleFile, 406 | workingDirectory = folder.root, 407 | modulePathAsString = modulePath, 408 | moduleType = KOTLIN, 409 | showErrorDialog = { 410 | Assert.fail("No errors should be thrown") 411 | }, 412 | showSuccessDialog = { 413 | assert(true) 414 | }, 415 | enhancedModuleCreationStrategy = false, 416 | useKtsBuildFile = false, 417 | gradleFileFollowModule = false, 418 | packageName = testPackageName, 419 | addReadme = false, 420 | addGitIgnore = true, 421 | rootPathString = folder.root.toString(), 422 | previewMode = false 423 | 424 | ) 425 | 426 | val settingsGradleFileContents = readFromFile(file = settingsGradleFile) 427 | assertEquals( 428 | "include(\"$modulePath\", \"$rootPathString/$modulePathAsFile\")", 429 | settingsGradleFileContents[56] 430 | ) 431 | } 432 | 433 | @Test 434 | fun `create module works with 2 parameters and custom include for modules`() { 435 | settingsGradleFile.delete() 436 | settingsGradleFile = folder.populateSettingsGradleKtsWithFakeFilePathDataAndCustomInclude() 437 | val modulePath = ":repository:network" 438 | val modulePathAsFile = "repository/network" 439 | val rootPathString = folder.root.toString().removePrefix("/") 440 | 441 | fileWriter.createModule( 442 | settingsGradleFile = settingsGradleFile, 443 | workingDirectory = folder.root, 444 | modulePathAsString = modulePath, 445 | moduleType = KOTLIN, 446 | showErrorDialog = { 447 | Assert.fail("No errors should be thrown") 448 | }, 449 | showSuccessDialog = { 450 | assert(true) 451 | }, 452 | enhancedModuleCreationStrategy = false, 453 | useKtsBuildFile = false, 454 | gradleFileFollowModule = false, 455 | packageName = testPackageName, 456 | addReadme = false, 457 | addGitIgnore = true, 458 | rootPathString = folder.root.toString(), 459 | previewMode = false 460 | 461 | ) 462 | 463 | val settingsGradleFileContents = readFromFile(file = settingsGradleFile) 464 | assertEquals( 465 | "includeBuild(\"$modulePath\", \"$rootPathString/$modulePathAsFile\")", 466 | settingsGradleFileContents[56] 467 | ) 468 | } 469 | 470 | @Test 471 | fun `module added correctly settings gradle with one big include`() { 472 | settingsGradleFile.delete() 473 | settingsGradleFile = folder.populateSettingsGradleKtsWithTiviSettingsGradleKts() 474 | val modulePath = ":repository:network" 475 | 476 | fileWriter.createModule( 477 | settingsGradleFile = settingsGradleFile, 478 | workingDirectory = folder.root, 479 | modulePathAsString = modulePath, 480 | moduleType = KOTLIN, 481 | showErrorDialog = { 482 | Assert.fail("No errors should be thrown") 483 | }, 484 | showSuccessDialog = { 485 | assert(true) 486 | }, 487 | enhancedModuleCreationStrategy = false, 488 | useKtsBuildFile = false, 489 | gradleFileFollowModule = false, 490 | packageName = testPackageName, 491 | addReadme = false, 492 | addGitIgnore = true, 493 | rootPathString = folder.root.toString(), 494 | previewMode = false 495 | 496 | ) 497 | 498 | val settingsGradleFileContents = readFromFile(file = settingsGradleFile) 499 | assertEquals( 500 | "include(\"$modulePath\")", 501 | settingsGradleFileContents[45] 502 | ) 503 | } 504 | 505 | @Test 506 | fun `custom include keyword is used when specified`() { 507 | settingsGradleFile.delete() 508 | settingsGradleFile = folder.populateSettingsGradleKtsWithTiviWithCustomIncludeSettingsGradleKts() 509 | val modulePath = ":repository:network" 510 | 511 | fakePreferenceService.preferenceState.includeProjectKeyword = "testIncludeProject" 512 | 513 | fileWriter.createModule( 514 | settingsGradleFile = settingsGradleFile, 515 | workingDirectory = folder.root, 516 | modulePathAsString = modulePath, 517 | moduleType = KOTLIN, 518 | showErrorDialog = { 519 | Assert.fail("No errors should be thrown") 520 | }, 521 | showSuccessDialog = { 522 | assert(true) 523 | }, 524 | enhancedModuleCreationStrategy = false, 525 | useKtsBuildFile = false, 526 | gradleFileFollowModule = false, 527 | packageName = testPackageName, 528 | addReadme = false, 529 | addGitIgnore = true, 530 | rootPathString = folder.root.toString(), 531 | previewMode = false 532 | 533 | ) 534 | 535 | val settingsGradleFileContents = readFromFile(file = settingsGradleFile) 536 | assertEquals( 537 | "testIncludeProject(\"$modulePath\")", 538 | settingsGradleFileContents[45] 539 | ) 540 | } 541 | 542 | @Test 543 | fun `module added correctly settings gradle with one include statement`() { 544 | settingsGradleFile.delete() 545 | settingsGradleFile = folder.populateSettingsGradleWithFakeData() 546 | val modulePath = ":repository:network" 547 | 548 | fileWriter.createModule( 549 | settingsGradleFile = settingsGradleFile, 550 | workingDirectory = folder.root, 551 | modulePathAsString = modulePath, 552 | moduleType = KOTLIN, 553 | showErrorDialog = { 554 | Assert.fail("No errors should be thrown") 555 | }, 556 | showSuccessDialog = { 557 | assert(true) 558 | }, 559 | enhancedModuleCreationStrategy = false, 560 | useKtsBuildFile = false, 561 | gradleFileFollowModule = false, 562 | packageName = testPackageName, 563 | addReadme = false, 564 | addGitIgnore = true, 565 | rootPathString = folder.root.toString(), 566 | previewMode = false 567 | 568 | ) 569 | 570 | val settingsGradleFileContents = readFromFile(file = settingsGradleFile) 571 | assertEquals( 572 | "include(\"$modulePath\")", 573 | settingsGradleFileContents[16] 574 | ) 575 | } 576 | 577 | @Test 578 | fun `no files created in preview mode`() { 579 | settingsGradleFile.delete() 580 | settingsGradleFile = folder.populateSettingsGradleWithFakeData() 581 | val modulePath = ":repository:network" 582 | 583 | val rootFiles = folder.root.listFiles() 584 | 585 | val settingsGradleFileContentsBefore = readFromFile(file = settingsGradleFile) 586 | 587 | val filesToReturn = fileWriter.createModule( 588 | settingsGradleFile = settingsGradleFile, 589 | workingDirectory = folder.root, 590 | modulePathAsString = modulePath, 591 | moduleType = KOTLIN, 592 | showErrorDialog = { 593 | Assert.fail("No errors should be thrown") 594 | }, 595 | showSuccessDialog = { 596 | assert(true) 597 | }, 598 | enhancedModuleCreationStrategy = false, 599 | useKtsBuildFile = false, 600 | gradleFileFollowModule = false, 601 | packageName = testPackageName, 602 | addReadme = false, 603 | addGitIgnore = true, 604 | rootPathString = folder.root.toString(), 605 | previewMode = true 606 | 607 | ) 608 | 609 | val settingsGradleFileContentsAfter = readFromFile(file = settingsGradleFile) 610 | 611 | assertEquals( 612 | settingsGradleFileContentsBefore, 613 | settingsGradleFileContentsAfter 614 | ) 615 | 616 | assertEquals( 617 | filesToReturn.size, 618 | 3 619 | ) 620 | 621 | assertEquals( 622 | rootFiles!!.size, 623 | folder.root.listFiles()!!.size 624 | ) 625 | } 626 | } 627 | -------------------------------------------------------------------------------- /src/test/kotlin/com/joetr/modulemaker/TestConstants.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker 2 | 3 | const val settingsGradleKts = "settings.gradle.kts" 4 | const val settingsGradle = "settings.gradle" 5 | const val readmeFile = "README.md" 6 | const val buildGradleFileName = "build.gradle" 7 | const val buildGradleKtsFileName = "build.gradle.kts" 8 | const val testPackageName = "com.joetr.test" 9 | -------------------------------------------------------------------------------- /src/test/kotlin/com/joetr/modulemaker/TestUtilities.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker 2 | 3 | import com.joetr.modulemaker.settings.DefaultTemplateSettingsGradle 4 | import com.joetr.modulemaker.settings.NowInAndroidSettingsGradleKts 5 | import com.joetr.modulemaker.settings.TiviSettingsGradleKts 6 | import org.junit.rules.TemporaryFolder 7 | import java.io.BufferedReader 8 | import java.io.File 9 | import java.io.FileReader 10 | import java.io.FileWriter 11 | 12 | fun TemporaryFolder.populateSettingsGradleKtsWithFakeData(): File { 13 | val settingsGradleKts = this.newFile(settingsGradleKts) 14 | val writer = FileWriter(settingsGradleKts) 15 | writer.write(NowInAndroidSettingsGradleKts.data) 16 | writer.close() 17 | return settingsGradleKts 18 | } 19 | 20 | fun TemporaryFolder.populateSettingsGradleKtsWithFakeFilePathData(): File { 21 | val settingsGradleKts = this.newFile(settingsGradleKts) 22 | val writer = FileWriter(settingsGradleKts) 23 | writer.write(NowInAndroidSettingsGradleKts.filePathData) 24 | writer.close() 25 | return settingsGradleKts 26 | } 27 | 28 | fun TemporaryFolder.populateSettingsGradleKtsWithFakeFilePathDataAndCustomInclude(): File { 29 | val settingsGradleKts = this.newFile(settingsGradleKts) 30 | val writer = FileWriter(settingsGradleKts) 31 | writer.write(NowInAndroidSettingsGradleKts.filePathDataWithCustomIncludeBuildData) 32 | writer.close() 33 | return settingsGradleKts 34 | } 35 | 36 | fun TemporaryFolder.populateSettingsGradleKtsWithTiviSettingsGradleKts(): File { 37 | val settingsGradleKts = this.newFile(settingsGradleKts) 38 | val writer = FileWriter(settingsGradleKts) 39 | writer.write(TiviSettingsGradleKts.data) 40 | writer.close() 41 | return settingsGradleKts 42 | } 43 | 44 | fun TemporaryFolder.populateSettingsGradleKtsWithTiviWithCustomIncludeSettingsGradleKts(): File { 45 | val settingsGradleKts = this.newFile(settingsGradleKts) 46 | val writer = FileWriter(settingsGradleKts) 47 | writer.write(TiviSettingsGradleKts.dataWithCustomIncludeProject) 48 | writer.close() 49 | return settingsGradleKts 50 | } 51 | 52 | fun TemporaryFolder.populateSettingsGradleWithFakeData(): File { 53 | val settingsGradle = this.newFile(settingsGradle) 54 | val writer = FileWriter(settingsGradle) 55 | writer.write(DefaultTemplateSettingsGradle.data) 56 | writer.close() 57 | return settingsGradle 58 | } 59 | 60 | fun readFromFile(file: File): List { 61 | val fileReader = FileReader(file) 62 | val bufferedReader = BufferedReader(fileReader) 63 | val data = mutableListOf() 64 | try { 65 | var line: String? 66 | while (bufferedReader.readLine().also { line = it } != null) { 67 | data.add(line.orEmpty()) 68 | } 69 | } finally { 70 | fileReader.close() 71 | bufferedReader.close() 72 | } 73 | return data 74 | } 75 | -------------------------------------------------------------------------------- /src/test/kotlin/com/joetr/modulemaker/settings/DefaultTemplateSettingsGradle.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.settings 2 | 3 | object DefaultTemplateSettingsGradle { 4 | val data = """ 5 | pluginManagement { 6 | repositories { 7 | google() 8 | mavenCentral() 9 | gradlePluginPortal() 10 | } 11 | } 12 | dependencyResolutionManagement { 13 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 14 | repositories { 15 | google() 16 | mavenCentral() 17 | } 18 | } 19 | rootProject.name = "ModuleMakerTest" 20 | include ':app' 21 | """.trimIndent() 22 | } 23 | -------------------------------------------------------------------------------- /src/test/kotlin/com/joetr/modulemaker/settings/NowInAndroidSettingsGradleKts.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.settings 2 | 3 | object NowInAndroidSettingsGradleKts { 4 | val data = """ 5 | /* 6 | * Copyright 2021 The Android Open Source Project 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * https://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | pluginManagement { 22 | includeBuild("build-logic") 23 | repositories { 24 | google() 25 | mavenCentral() 26 | gradlePluginPortal() 27 | } 28 | } 29 | 30 | dependencyResolutionManagement { 31 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 32 | repositories { 33 | google() 34 | mavenCentral() 35 | } 36 | } 37 | rootProject.name = "nowinandroid" 38 | include(":app") 39 | include(":app-nia-catalog") 40 | include(":benchmarks") 41 | include(":core:common") 42 | include(":core:data") 43 | include(":core:data-test") 44 | include(":core:database") 45 | include(":core:datastore") 46 | include(":core:datastore-test") 47 | include(":core:designsystem") 48 | include(":core:domain") 49 | include(":core:model") 50 | include(":core:network") 51 | include(":core:ui") 52 | include(":core:testing") 53 | include(":core:analytics") 54 | include(":core:notifications") 55 | 56 | include(":feature:foryou") 57 | include(":feature:interests") 58 | include(":feature:bookmarks") 59 | include(":feature:topic") 60 | include(":feature:settings") 61 | include(":lint") 62 | include(":sync:work") 63 | include(":sync:sync-test") 64 | include(":ui-test-hilt-manifest") 65 | """.trimIndent() 66 | 67 | val filePathData = """ 68 | /* 69 | * Copyright 2021 The Android Open Source Project 70 | * 71 | * Licensed under the Apache License, Version 2.0 (the "License"); 72 | * you may not use this file except in compliance with the License. 73 | * You may obtain a copy of the License at 74 | * 75 | * https://www.apache.org/licenses/LICENSE-2.0 76 | * 77 | * Unless required by applicable law or agreed to in writing, software 78 | * distributed under the License is distributed on an "AS IS" BASIS, 79 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 80 | * See the License for the specific language governing permissions and 81 | * limitations under the License. 82 | */ 83 | 84 | pluginManagement { 85 | includeBuild("build-logic") 86 | repositories { 87 | google() 88 | mavenCentral() 89 | gradlePluginPortal() 90 | } 91 | } 92 | 93 | dependencyResolutionManagement { 94 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 95 | repositories { 96 | google() 97 | mavenCentral() 98 | } 99 | } 100 | rootProject.name = "nowinandroid" 101 | include(":app", "path/to") 102 | include(":app-nia-catalog", "path/to") 103 | include(":benchmarks", "path/to") 104 | include(":core:common", "path/to") 105 | include(":core:data", "path/to") 106 | include(":core:data-test", "path/to") 107 | include(":core:database", "path/to") 108 | include(":core:datastore", "path/to") 109 | include(":core:datastore-test", "path/to") 110 | include(":core:designsystem", "path/to") 111 | include(":core:domain", "path/to") 112 | include(":core:model", "path/to") 113 | include(":core:network", "path/to") 114 | include(":core:ui", "path/to") 115 | include(":core:testing", "path/to") 116 | include(":core:analytics", "path/to") 117 | include(":core:notifications", "path/to)" 118 | 119 | include(":feature:foryou", "path/to") 120 | include(":feature:interests", "path/to") 121 | include(":feature:bookmarks", "path/to") 122 | include(":feature:topic", "path/to") 123 | include(":feature:settings", "path/to") 124 | include(":lint", "path/to") 125 | include(":sync:work", "path/to") 126 | include(":sync:sync-test", "path/to") 127 | include(":ui-test-hilt-manifest", "path/to") 128 | """.trimIndent() 129 | 130 | val filePathDataWithCustomIncludeBuildData = """ 131 | /* 132 | * Copyright 2021 The Android Open Source Project 133 | * 134 | * Licensed under the Apache License, Version 2.0 (the "License"); 135 | * you may not use this file except in compliance with the License. 136 | * You may obtain a copy of the License at 137 | * 138 | * https://www.apache.org/licenses/LICENSE-2.0 139 | * 140 | * Unless required by applicable law or agreed to in writing, software 141 | * distributed under the License is distributed on an "AS IS" BASIS, 142 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 143 | * See the License for the specific language governing permissions and 144 | * limitations under the License. 145 | */ 146 | 147 | pluginManagement { 148 | includeBuild("build-logic") 149 | repositories { 150 | google() 151 | mavenCentral() 152 | gradlePluginPortal() 153 | } 154 | } 155 | 156 | dependencyResolutionManagement { 157 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 158 | repositories { 159 | google() 160 | mavenCentral() 161 | } 162 | } 163 | rootProject.name = "nowinandroid" 164 | includeBuild(":app", "path/to") 165 | includeBuild(":app-nia-catalog", "path/to") 166 | includeBuild(":benchmarks", "path/to") 167 | includeBuild(":core:common", "path/to") 168 | includeBuild(":core:data", "path/to") 169 | includeBuild(":core:data-test", "path/to") 170 | includeBuild(":core:database", "path/to") 171 | includeBuild(":core:datastore", "path/to") 172 | includeBuild(":core:datastore-test", "path/to") 173 | includeBuild(":core:designsystem", "path/to") 174 | includeBuild(":core:domain", "path/to") 175 | includeBuild(":core:model", "path/to") 176 | includeBuild(":core:network", "path/to") 177 | includeBuild(":core:ui", "path/to") 178 | includeBuild(":core:testing", "path/to") 179 | includeBuild(":core:analytics", "path/to") 180 | includeBuild(":core:notifications", "path/to)" 181 | 182 | includeBuild(":feature:foryou", "path/to") 183 | includeBuild(":feature:interests", "path/to") 184 | includeBuild(":feature:bookmarks", "path/to") 185 | includeBuild(":feature:topic", "path/to") 186 | includeBuild(":feature:settings", "path/to") 187 | includeBuild(":lint", "path/to") 188 | includeBuild(":sync:work", "path/to") 189 | includeBuild(":sync:sync-test", "path/to") 190 | includeBuild(":ui-test-hilt-manifest", "path/to") 191 | """.trimIndent() 192 | } 193 | -------------------------------------------------------------------------------- /src/test/kotlin/com/joetr/modulemaker/settings/TiviSettingsGradleKts.kt: -------------------------------------------------------------------------------- 1 | package com.joetr.modulemaker.settings 2 | 3 | object TiviSettingsGradleKts { 4 | val data = """ 5 | // Copyright 2023, Christopher Banes and the Tivi project contributors 6 | // SPDX-License-Identifier: Apache-2.0 7 | 8 | pluginManagement { 9 | includeBuild("gradle/build-logic") 10 | 11 | repositories { 12 | gradlePluginPortal() 13 | google() 14 | mavenCentral() 15 | } 16 | } 17 | 18 | dependencyResolutionManagement { 19 | repositories { 20 | google() 21 | mavenCentral() 22 | mavenLocal() 23 | 24 | // Needed when using the 'dev' Compose Compiler 25 | // maven("https://androidx.dev/storage/compose-compiler/repository/") 26 | 27 | // Used for snapshots if needed 28 | // maven("https://oss.sonatype.org/content/repositories/snapshots/") 29 | // maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") 30 | } 31 | } 32 | 33 | plugins { 34 | id("com.gradle.enterprise") version "3.13.4" 35 | } 36 | 37 | gradleEnterprise { 38 | buildScan { 39 | termsOfServiceUrl = "https://gradle.com/terms-of-service" 40 | termsOfServiceAgree = "yes" 41 | } 42 | } 43 | 44 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 45 | // https://docs.gradle.org/7.6/userguide/configuration_cache.html#config_cache:stable 46 | // enableFeaturePreview("STABLE_CONFIGURATION_CACHE") 47 | 48 | rootProject.name = "tivi" 49 | 50 | include( 51 | ":core:analytics", 52 | ":core:base", 53 | ":core:logging", 54 | ":core:performance", 55 | ":core:powercontroller", 56 | ":core:preferences", 57 | ":common:ui:circuit-overlay", 58 | ":common:ui:resources", 59 | ":common:ui:strings", 60 | ":common:ui:compose", 61 | ":common:ui:screens", 62 | ":common:imageloading", 63 | ":data:db", 64 | ":data:db-sqldelight", 65 | ":data:legacy", 66 | ":data:models", 67 | ":data:test", 68 | ":data:episodes", 69 | ":data:followedshows", 70 | ":data:popularshows", 71 | ":data:recommendedshows", 72 | ":data:relatedshows", 73 | ":data:search", 74 | ":data:shows", 75 | ":data:showimages", 76 | ":data:traktauth", 77 | ":data:traktusers", 78 | ":data:trendingshows", 79 | ":data:watchedshows", 80 | ":api:trakt", 81 | ":api:tmdb", 82 | ":tasks", 83 | ":domain", 84 | ":shared", 85 | ":ui:discover", 86 | ":ui:episode:details", 87 | ":ui:episode:track", 88 | ":ui:trending", 89 | ":ui:popular", 90 | ":ui:recommended", 91 | ":ui:search", 92 | ":ui:show:details", 93 | ":ui:show:seasons", 94 | ":ui:settings", 95 | ":ui:library", 96 | ":ui:account", 97 | ":ui:upnext", 98 | ":ui:root", 99 | ":android-app:app", 100 | ":android-app:benchmark", 101 | ":android-app:common-test", 102 | ":desktop-app", 103 | ":thirdparty:swipe", 104 | ":thirdparty:compose-material-dialogs:datetime", 105 | ) 106 | """.trimIndent() 107 | 108 | val dataWithCustomIncludeProject = """ 109 | // Copyright 2023, Christopher Banes and the Tivi project contributors 110 | // SPDX-License-Identifier: Apache-2.0 111 | 112 | pluginManagement { 113 | includeBuild("gradle/build-logic") 114 | 115 | repositories { 116 | gradlePluginPortal() 117 | google() 118 | mavenCentral() 119 | } 120 | } 121 | 122 | dependencyResolutionManagement { 123 | repositories { 124 | google() 125 | mavenCentral() 126 | mavenLocal() 127 | 128 | // Needed when using the 'dev' Compose Compiler 129 | // maven("https://androidx.dev/storage/compose-compiler/repository/") 130 | 131 | // Used for snapshots if needed 132 | // maven("https://oss.sonatype.org/content/repositories/snapshots/") 133 | // maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") 134 | } 135 | } 136 | 137 | plugins { 138 | id("com.gradle.enterprise") version "3.13.4" 139 | } 140 | 141 | gradleEnterprise { 142 | buildScan { 143 | termsOfServiceUrl = "https://gradle.com/terms-of-service" 144 | termsOfServiceAgree = "yes" 145 | } 146 | } 147 | 148 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 149 | // https://docs.gradle.org/7.6/userguide/configuration_cache.html#config_cache:stable 150 | // enableFeaturePreview("STABLE_CONFIGURATION_CACHE") 151 | 152 | rootProject.name = "tivi" 153 | 154 | testIncludeProject( 155 | ":core:analytics", 156 | ":core:base", 157 | ":core:logging", 158 | ":core:performance", 159 | ":core:powercontroller", 160 | ":core:preferences", 161 | ":common:ui:circuit-overlay", 162 | ":common:ui:resources", 163 | ":common:ui:strings", 164 | ":common:ui:compose", 165 | ":common:ui:screens", 166 | ":common:imageloading", 167 | ":data:db", 168 | ":data:db-sqldelight", 169 | ":data:legacy", 170 | ":data:models", 171 | ":data:test", 172 | ":data:episodes", 173 | ":data:followedshows", 174 | ":data:popularshows", 175 | ":data:recommendedshows", 176 | ":data:relatedshows", 177 | ":data:search", 178 | ":data:shows", 179 | ":data:showimages", 180 | ":data:traktauth", 181 | ":data:traktusers", 182 | ":data:trendingshows", 183 | ":data:watchedshows", 184 | ":api:trakt", 185 | ":api:tmdb", 186 | ":tasks", 187 | ":domain", 188 | ":shared", 189 | ":ui:discover", 190 | ":ui:episode:details", 191 | ":ui:episode:track", 192 | ":ui:trending", 193 | ":ui:popular", 194 | ":ui:recommended", 195 | ":ui:search", 196 | ":ui:show:details", 197 | ":ui:show:seasons", 198 | ":ui:settings", 199 | ":ui:library", 200 | ":ui:account", 201 | ":ui:upnext", 202 | ":ui:root", 203 | ":android-app:app", 204 | ":android-app:benchmark", 205 | ":android-app:common-test", 206 | ":desktop-app", 207 | ":thirdparty:swipe", 208 | ":thirdparty:compose-material-dialogs:datetime", 209 | ) 210 | """.trimIndent() 211 | } 212 | --------------------------------------------------------------------------------