Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ As of the `0.8.0` release, example apps for testing are included when initializi
--author <author> ......... Author name and email (e.g. "Name <name@example.com>")
--license <id> ............ SPDX License ID (e.g. "MIT")
--description <text> ...... Short description of plugin features
--android-lang <text> ..... Language for Android plugin development (either "kotlin" or "java")
```
13 changes: 13 additions & 0 deletions assets/plugin-template/README.md.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
21 changes: 21 additions & 0 deletions assets/plugin-template/android/build.gradle.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
Expand All @@ -42,6 +54,12 @@ android {
}
}

{{ #KOTLIN }}
kotlin {
jvmToolchain(21)
}
{{ /KOTLIN }}

repositories {
google()
mavenCentral()
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@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)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
class ExampleUnitTest {

@Test
@Throws(Exception::class)
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
1 change: 1 addition & 0 deletions src/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const help = `
--author <author> ......... Author name and email (e.g. "Name <name@example.com>")
--license <id> ............ SPDX License ID (e.g. "MIT")
--description <text> ...... 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
Expand Down
16 changes: 15 additions & 1 deletion src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface OptionValues {
author: string;
license: string;
description: string;
'android-lang': string;
}

export type Validators = {
Expand All @@ -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) =>
Expand Down Expand Up @@ -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"`
Expand Down
9 changes: 9 additions & 0 deletions src/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ export const gatherDetails = (initialOptions: Options): Promise<OptionValues> =>
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',
Expand Down
64 changes: 56 additions & 8 deletions src/template.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -25,6 +25,7 @@ export const extractTemplate = async (
): Promise<void> => {
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,
Expand All @@ -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<void> => {
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<boolean> => {
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<void> => {
const template = await readFile(p, { encoding: 'utf8' });

const conditionalView = {
KOTLIN: androidLang.toLowerCase() !== 'java',
};

const view = {
CAPACITOR_VERSION: CAPACITOR_VERSION,
PACKAGE_NAME: name,
Expand All @@ -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);
};
Expand Down