diff --git a/README.md b/README.md index f055a05..dd029a1 100644 --- a/README.md +++ b/README.md @@ -30,4 +30,5 @@ As of the `0.8.0` release, example apps for testing are included when initializi --author ......... Author name and email (e.g. "Name ") --license ............ SPDX License ID (e.g. "MIT") --description ...... Short description of plugin features +--android-lang ..... Language for Android plugin development (either "kotlin" or "java") ``` diff --git a/assets/plugin-template/README.md.mustache b/assets/plugin-template/README.md.mustache index 837a83f..eab36d9 100644 --- a/assets/plugin-template/README.md.mustache +++ b/assets/plugin-template/README.md.mustache @@ -4,8 +4,21 @@ ## Install +To use npm + ```bash npm install {{{ PACKAGE_NAME }}} +```` + +To use yarn + +```bash +yarn add {{{ PACKAGE_NAME }}} +``` + +Sync native files + +```bash npx cap sync ``` diff --git a/assets/plugin-template/android/build.gradle.mustache b/assets/plugin-template/android/build.gradle.mustache index 814dcb1..a2abf04 100644 --- a/assets/plugin-template/android/build.gradle.mustache +++ b/assets/plugin-template/android/build.gradle.mustache @@ -3,19 +3,31 @@ ext { androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1' androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0' androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0' +{{ #KOTLIN }} + androidxCoreKTXVersion = project.hasProperty('androidxCoreKTXVersion') ? rootProject.ext.androidxCoreKTXVersion : '1.17.0' +{{ /KOTLIN }} } buildscript { +{{ #KOTLIN }} + ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '2.2.20' +{{ /KOTLIN }} repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:8.13.0' +{{ #KOTLIN }} + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" +{{ /KOTLIN }} } } apply plugin: 'com.android.library' +{{ #KOTLIN }} +apply plugin: 'kotlin-android' +{{ /KOTLIN }} android { namespace = "{{ PACKAGE_ID }}" @@ -42,6 +54,12 @@ android { } } +{{ #KOTLIN }} +kotlin { + jvmToolchain(21) +} +{{ /KOTLIN }} + repositories { google() mavenCentral() @@ -52,6 +70,9 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':capacitor-android') implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" +{{ #KOTLIN }} + implementation "androidx.core:core-ktx:$androidxCoreKTXVersion" +{{ /KOTLIN }} testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" diff --git a/assets/plugin-template/android/src/androidTest/kotlin/com/getcapacitor/android/ExampleInstrumentedTest.kt b/assets/plugin-template/android/src/androidTest/kotlin/com/getcapacitor/android/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..8e8e3f0 --- /dev/null +++ b/assets/plugin-template/android/src/androidTest/kotlin/com/getcapacitor/android/ExampleInstrumentedTest.kt @@ -0,0 +1,26 @@ +package com.getcapacitor.android + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + + @Test + @Throws(Exception::class) + fun useAppContext() { + // Context of the app under test. + val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext + + assertEquals("com.getcapacitor.android", appContext.packageName) + } +} diff --git a/assets/plugin-template/android/src/main/kotlin/__JAVA_PATH__.kt.mustache b/assets/plugin-template/android/src/main/kotlin/__JAVA_PATH__.kt.mustache new file mode 100644 index 0000000..341ff69 --- /dev/null +++ b/assets/plugin-template/android/src/main/kotlin/__JAVA_PATH__.kt.mustache @@ -0,0 +1,12 @@ +package {{ PACKAGE_ID }} + +import android.util.Log + +class {{ CLASS }} { + + fun echo(value: String?): String? { + Log.i("Echo", value ?: "null") + + return value + } +} diff --git a/assets/plugin-template/android/src/main/kotlin/__JAVA_PATH__Plugin.kt.mustache b/assets/plugin-template/android/src/main/kotlin/__JAVA_PATH__Plugin.kt.mustache new file mode 100644 index 0000000..52ee14c --- /dev/null +++ b/assets/plugin-template/android/src/main/kotlin/__JAVA_PATH__Plugin.kt.mustache @@ -0,0 +1,23 @@ +package {{ PACKAGE_ID }} + +import com.getcapacitor.JSObject +import com.getcapacitor.Plugin +import com.getcapacitor.PluginCall +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.PluginMethod + +@CapacitorPlugin(name = "{{ CLASS }}") +class {{ CLASS }}Plugin : Plugin() { + + private val implementation = {{ CLASS }}() + + @PluginMethod + fun echo(call: PluginCall) { + val value = call.getString("value") + + val ret = JSObject().apply { + put("value", implementation.echo(value)) + } + call.resolve(ret) + } +} diff --git a/assets/plugin-template/android/src/test/kotlin/com/getcapacitor/ExampleUnitTest.kt b/assets/plugin-template/android/src/test/kotlin/com/getcapacitor/ExampleUnitTest.kt new file mode 100644 index 0000000..ba1894e --- /dev/null +++ b/assets/plugin-template/android/src/test/kotlin/com/getcapacitor/ExampleUnitTest.kt @@ -0,0 +1,18 @@ +package com.getcapacitor + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +class ExampleUnitTest { + + @Test + @Throws(Exception::class) + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/src/help.ts b/src/help.ts index db53697..df38983 100644 --- a/src/help.ts +++ b/src/help.ts @@ -10,6 +10,7 @@ const help = ` --author ......... Author name and email (e.g. "Name ") --license ............ SPDX License ID (e.g. "MIT") --description ...... Short description of plugin features + --android-lang ............ Language for Android plugin development (either "kotlin" or "java") -h, --help ................ Print help, then quit --verbose ................. Print verbose output to stderr diff --git a/src/options.ts b/src/options.ts index cc852bc..e15a25d 100644 --- a/src/options.ts +++ b/src/options.ts @@ -17,6 +17,7 @@ export interface OptionValues { author: string; license: string; description: string; + 'android-lang': string; } export type Validators = { @@ -25,7 +26,16 @@ export type Validators = { const CLI_ARGS = ['dir'] as const; -const CLI_OPTIONS = ['name', 'package-id', 'class-name', 'repo', 'author', 'license', 'description'] as const; +const CLI_OPTIONS = [ + 'name', + 'package-id', + 'class-name', + 'repo', + 'author', + 'license', + 'description', + 'android-lang', +] as const; export const VALIDATORS: Validators = { name: (value) => @@ -57,6 +67,10 @@ export const VALIDATORS: Validators = { typeof value !== 'string' || value.trim().length === 0 ? `Must provide a valid license, e.g. "MIT"` : true, description: (value) => typeof value !== 'string' || value.trim().length === 0 ? `Must provide a description` : true, + 'android-lang': (value) => + typeof value === 'string' && value.trim().length > 0 && /^(kotlin|kt|java)$/i.test(value) + ? true + : `Must be either "kotlin" or "java"`, dir: (value) => typeof value !== 'string' || value.trim().length === 0 ? `Must provide a directory, e.g. "my-plugin"` diff --git a/src/prompt.ts b/src/prompt.ts index 8b49e86..76d601f 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -81,6 +81,15 @@ export const gatherDetails = (initialOptions: Options): Promise => message: `Enter a SPDX license identifier for your plugin.\n`, validate: VALIDATORS.license, }, + { + type: 'select', + name: 'android-lang', + message: `What language would you like to use for your Android plugin?\n`, + choices: [ + { title: 'Kotlin', value: 'kotlin' }, + { title: 'Java', value: 'java' }, + ], + }, { type: 'text', name: 'description', diff --git a/src/template.ts b/src/template.ts index 37e78fa..6446f8e 100644 --- a/src/template.ts +++ b/src/template.ts @@ -1,4 +1,4 @@ -import { readFile, rmdir, mkdir, writeFile, unlink } from 'fs/promises'; +import { readFile, mkdir, writeFile, unlink, readdir, rm } from 'fs/promises'; import Mustache from 'mustache'; import { dirname, join, resolve, sep } from 'path'; import { extract } from 'tar'; @@ -25,6 +25,7 @@ export const extractTemplate = async ( ): Promise => { const templateFiles: string[] = []; const templateFolders: string[] = []; + const androidLang = details['android-lang'].toLowerCase(); await mkdir(dir, { recursive: true }); await extract({ file: type === 'PLUGIN_TEMPLATE' ? TEMPLATE_PATH : WWW_TEMPLATE_PATH, @@ -41,14 +42,57 @@ export const extractTemplate = async ( }); await Promise.all(templateFiles.map((p) => resolve(dir, p)).map((p) => applyTemplate(p, details))); - await Promise.all(templateFolders.map((p) => resolve(dir, p)).map((p) => rmdir(p))); + await deleteUnnecessaryFolders(dir, androidLang); + await Promise.all(templateFolders.map((p) => resolve(dir, p)).map((p) => rm(p, { recursive: true }))); +}; + +const deleteUnnecessaryFolders = async (dir: string, androidLang: string): Promise => { + const androidSrcDir = join(dir, 'android', 'src'); + const sourceSets = ['main', 'test', 'androidTest']; + + for (const sourceSet of sourceSets) { + const sourceFolder = join(androidSrcDir, sourceSet); + const javaFolder = join(sourceFolder, 'java'); + const kotlinFolder = join(sourceFolder, 'kotlin'); + + if (androidLang === 'kotlin' && (await folderExists(javaFolder))) { + await rm(javaFolder, { recursive: true }); + } + + if (androidLang === 'java' && (await folderExists(kotlinFolder))) { + await rm(kotlinFolder, { recursive: true }); + } + } +}; + +const folderExists = async (folderPath: string): Promise => { + try { + const files = await readdir(folderPath); + return files != null; + } catch (err) { + return false; + } }; export const applyTemplate = async ( p: string, - { name, 'package-id': packageId, 'class-name': className, repo, author, license, description }: OptionValues, + { + name, + 'package-id': packageId, + 'class-name': className, + repo, + author, + license, + description, + 'android-lang': androidLang, + }: OptionValues, ): Promise => { const template = await readFile(p, { encoding: 'utf8' }); + + const conditionalView = { + KOTLIN: androidLang.toLowerCase() !== 'java', + }; + const view = { CAPACITOR_VERSION: CAPACITOR_VERSION, PACKAGE_NAME: name, @@ -60,17 +104,21 @@ export const applyTemplate = async ( AUTHOR: author, LICENSE: license, DESCRIPTION: description, + ANDROID_LANG: androidLang, }; - const contents = Mustache.render(template, view); - const filePath = Object.entries(view).reduce( - (acc, [key, value]) => (value ? acc.replaceAll(`__${key}__`, value) : acc), - p.substring(0, p.length - MUSTACHE_EXTENSION.length), + const combinedView = { ...view, ...conditionalView }; + const intermediateContents = Mustache.render(template, combinedView); + const finalContents = Mustache.render(intermediateContents, view); + let filePath = p.substring(0, p.length - MUSTACHE_EXTENSION.length); + filePath = Object.entries(view).reduce( + (acc, [key, value]) => (value ? acc.replaceAll(`__${key}__`, value.toString()) : acc), + filePath, ); await mkdir(dirname(filePath), { recursive: true }); // take off the .mustache extension and write the file, then remove the template - await writeFile(filePath, contents, { encoding: 'utf8' }); + await writeFile(filePath, finalContents, { encoding: 'utf8' }); await unlink(p); };