@aparajita/capacitor-native-decorator
Advanced tools
Comparing version 1.1.1 to 2.0.0
@@ -13,2 +13,4 @@ #! /usr/bin/env node | ||
// Generated by @aparajita/capacitor-native-decorator/make-ios-plugin | ||
CAP_PLUGIN(__plugin__, "__plugin__", | ||
@@ -18,49 +20,62 @@ __methods__ | ||
`; | ||
const pluginEntryTemplate = ' CAP_PLUGIN_METHOD(__method__, CAPPluginReturnPromise);'; | ||
const pluginNameRE = /class\s+\w+\s+extends\s+core\.WebPlugin\s+{\s*constructor\(\)\s*{\s*super\({\s*name:\s*(['"])(.+?)\1\s*,/m; | ||
const decoraterRE = /__decorate\(\[\s*native\(\)\s*],\s*(\w+)\.prototype,\s*"(.+?)",\s*null\);/gm; | ||
const pluginEntryTemplate = ' CAP_PLUGIN_METHOD(__method__, __type__);'; | ||
const pluginNameRE = /class\s+(\w+)(?:\$\d+)?\s+extends\s+core.WebPlugin\s+\{/mu; | ||
/* | ||
__decorate([ | ||
native() | ||
], BiometricAuthWeb$1.prototype, "checkBiometry", null) | ||
*/ | ||
const decoraterRE = /__decorate\s*\(\s*\[\s*(?:capacitorNativeDecorator\.)?native\s*\((?:(?:capacitorNativeDecorator\.)?PluginReturnType\.(?<returnType>\w+))?\)\s*\],\s*(?<pluginName>\w+)(?:\$\d+)?(?:\.prototype)?,\s*"(?<methodName>.+?)",\s*null\)/gmu; | ||
const iosPath = path_1.default.join('ios', 'Plugin'); | ||
const pluginMPath = path_1.default.join(iosPath, 'Plugin.m'); | ||
let pluginPath; | ||
function main() { | ||
checkPaths(); | ||
function fail(message) { | ||
console.error(`❌ ${message}`); | ||
process.exit(1); | ||
} | ||
function checkPath() { | ||
// Make sure the iOS plugin has been generated | ||
if (!(0, fs_1.existsSync)(iosPath)) { | ||
fail('Couldn’t find the ios plugin — did you run `capacitor add ios`?'); | ||
} | ||
let pluginPath = path_1.default.join('dist', 'plugin.js'); | ||
if (process.argv[2]) { | ||
pluginPath = process.argv[2]; | ||
} | ||
if (!(0, fs_1.existsSync)(pluginPath)) { | ||
fail('Couldn’t find the web plugin, run build first'); | ||
} | ||
return pluginPath; | ||
} | ||
function cli() { | ||
const pluginPath = checkPath(); | ||
try { | ||
const plugin = fs_1.readFileSync(pluginPath, 'utf-8'); | ||
const nameMatch = plugin.match(pluginNameRE); | ||
const plugin = (0, fs_1.readFileSync)(pluginPath, 'utf-8'); | ||
const nameMatch = pluginNameRE.exec(plugin); | ||
if (!nameMatch) { | ||
fail(`The plugin name could not be found in ${pluginPath}`); | ||
} | ||
const pluginName = nameMatch[2]; | ||
const pluginName = nameMatch[1]; | ||
const nativeMethods = [...plugin.matchAll(decoraterRE)].reduce((result, match) => { | ||
result.push(pluginEntryTemplate.replace('__method__', match[2])); | ||
const groups = match.groups; | ||
const returnType = ['none', 'callback'].includes(groups?.returnType ?? '') | ||
? 'CAPPluginReturnCallback' | ||
: 'CAPPluginReturnPromise'; | ||
result.push(pluginEntryTemplate | ||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers | ||
.replace('__method__', groups?.methodName ?? '') | ||
.replace('__type__', returnType)); | ||
return result; | ||
}, []); | ||
const template = pluginMTemplate | ||
.replace(/__plugin__/g, pluginName) | ||
.replace(/__plugin__/gu, pluginName) | ||
.replace('__methods__', nativeMethods.join('\n')); | ||
fs_1.writeFileSync(pluginMPath, template, { encoding: 'utf-8' }); | ||
(0, fs_1.writeFileSync)(pluginMPath, template, { encoding: 'utf-8' }); | ||
console.log(`✅ Created ${pluginMPath}`); | ||
} | ||
catch (e) { | ||
fail(e.message); | ||
fail(e instanceof Error ? e.message : 'Unknown error occurred'); | ||
} | ||
} | ||
function checkPaths() { | ||
// Make sure the iOS plugin has been generated | ||
if (!fs_1.existsSync(iosPath)) { | ||
fail('Couldn’t find the ios plugin — did you run `capacitor add ios`?'); | ||
} | ||
pluginPath = path_1.default.join('dist', 'plugin.js'); | ||
if (process.argv[2]) { | ||
pluginPath = process.argv[2]; | ||
} | ||
if (!fs_1.existsSync(pluginPath)) { | ||
fail('Couldn’t find the web plugin, run build first'); | ||
} | ||
} | ||
function fail(message) { | ||
console.error('❌ ' + message); | ||
process.exit(1); | ||
} | ||
main(); | ||
cli(); | ||
exports.default = cli; | ||
//# sourceMappingURL=make-ios-plugin.js.map |
@@ -1,8 +0,6 @@ | ||
import { PluginResultError } from '@capacitor/core'; | ||
import type { PluginResultError } from '@capacitor/core'; | ||
/** | ||
* The options that are passed to a native plugin. | ||
*/ | ||
export declare type CallOptions = { | ||
[key: string]: any; | ||
}; | ||
export declare type CallOptions = Record<string, never>; | ||
/** | ||
@@ -14,1 +12,12 @@ * Capacitor plugins may also return an error code. | ||
} | ||
/** | ||
* The type of plugin call. | ||
*/ | ||
export declare enum PluginReturnType { | ||
none = 0, | ||
promise = 1, | ||
callback = 2 | ||
} | ||
export interface DecoratedNativePlugin { | ||
getRegisteredPluginName: () => string; | ||
} |
@@ -1,2 +0,10 @@ | ||
export {}; | ||
/** | ||
* The type of plugin call. | ||
*/ | ||
export var PluginReturnType; | ||
(function (PluginReturnType) { | ||
PluginReturnType[PluginReturnType["none"] = 0] = "none"; | ||
PluginReturnType[PluginReturnType["promise"] = 1] = "promise"; | ||
PluginReturnType[PluginReturnType["callback"] = 2] = "callback"; | ||
})(PluginReturnType || (PluginReturnType = {})); | ||
//# sourceMappingURL=definitions.js.map |
@@ -1,1 +0,2 @@ | ||
export declare function native(): (target: any, methodName: string, descriptor: PropertyDescriptor) => PropertyDescriptor; | ||
import { PluginReturnType } from './definitions'; | ||
export declare function native(returnType?: PluginReturnType): (target: any, methodName: string, descriptor: PropertyDescriptor) => PropertyDescriptor; |
import { Capacitor } from '@capacitor/core'; | ||
const nativeMethodsProperty = '__native_methods__'; | ||
let nativeMethods; | ||
export function native() { | ||
return function (target, methodName, descriptor) { | ||
// target is the class prototype. If the class does not yet | ||
// have a __native_methods__ property, add it now. | ||
const targetClass = target.constructor; | ||
if (!targetClass.hasOwnProperty(nativeMethodsProperty)) { | ||
// We'll use a set to easily ensure that no matter how | ||
// many times the plugin is instantiated, the class property | ||
// will only contain the set of unique native method names. | ||
nativeMethods = new Set(); | ||
Object.defineProperty(targetClass, nativeMethodsProperty, { | ||
configurable: false, | ||
enumerable: false, | ||
writable: false, | ||
value: nativeMethods, | ||
}); | ||
} | ||
nativeMethods.add(methodName); | ||
const originalMethod = descriptor.value; | ||
descriptor.value = function (options) { | ||
if (Capacitor.isNative) { | ||
return callNativeMethod(this, methodName, options); | ||
} | ||
else { | ||
return originalMethod.call(this, options); | ||
} | ||
}; | ||
return descriptor; | ||
}; | ||
import { PluginReturnType } from './definitions'; | ||
/* eslint-enable */ | ||
function isFunction(value) { | ||
return typeof value === 'function'; | ||
} | ||
function callNativeMethod(plugin, methodName, options) { | ||
async function callNativePromise(name, methodName, options) { | ||
return new Promise((resolve, reject) => { | ||
@@ -41,3 +14,4 @@ const resolver = (data) => { | ||
if (keys.length === 1) { | ||
return resolve(data[keys[0]]); | ||
resolve(data[keys[0]]); | ||
return; | ||
} | ||
@@ -47,14 +21,95 @@ } | ||
}; | ||
// @ts-ignore - toNative() is only defined in native environments, | ||
// and this function is only called in native environments. | ||
Capacitor.toNative(plugin.config.name, methodName, options, { | ||
resolve: (data) => { | ||
resolver(data); | ||
}, | ||
reject: (error) => { | ||
reject(error); | ||
}, | ||
// Capacitor is actually of type CapacitorInstance | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
const cap = Capacitor; | ||
cap | ||
.nativePromise(name, methodName, options) | ||
.then((data) => { | ||
resolver(data); | ||
}) | ||
.catch((error) => { | ||
reject(error); | ||
}); | ||
}); | ||
} | ||
function wrappedFunction(pluginName, methodName, returnType, originalMethod) { | ||
return async function (optionsOrCallback, callback) { | ||
let options; | ||
let cb; | ||
if (typeof optionsOrCallback === 'object') { | ||
options = optionsOrCallback; | ||
if (typeof callback === 'function') { | ||
cb = callback; | ||
} | ||
} | ||
else if (typeof optionsOrCallback === 'function') { | ||
cb = optionsOrCallback; | ||
} | ||
if (Capacitor.isNativePlatform()) { | ||
if (returnType === PluginReturnType.promise) { | ||
return callNativePromise(pluginName, methodName, options); | ||
} | ||
else { | ||
// Capacitor is actually of type CapacitorInstance | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
const cap = Capacitor; | ||
return Promise.resolve(cap.nativeCallback(pluginName, methodName, options, cb)); | ||
} | ||
} | ||
else { | ||
// In the context of an instance method call | ||
// (which is what got us here), `this` is the instance. | ||
/* eslint-disable @typescript-eslint/no-unsafe-return,@typescript-eslint/no-invalid-this */ | ||
if (options) { | ||
if (cb) { | ||
// @ts-expect-error: See above comment | ||
return originalMethod.call(this, options, cb); | ||
} | ||
// @ts-expect-error: See above comment | ||
return originalMethod.call(this, options); | ||
} | ||
if (cb) { | ||
// @ts-expect-error: See above comment | ||
return originalMethod.call(this, cb); | ||
} | ||
// @ts-expect-error: See above comment | ||
return originalMethod.call(this); | ||
} | ||
}; | ||
} | ||
// This will be re-exported by index.ts | ||
// eslint-disable-next-line import/prefer-default-export | ||
export function native(returnType = PluginReturnType.promise) { | ||
return function ( | ||
// eslint-disable-next-line | ||
target, methodName, descriptor) { | ||
if (isFunction(descriptor.value)) { | ||
const originalMethod = descriptor.value; | ||
let prototype; | ||
// We only support instance methods, that's what Capacitor supports. | ||
// If target is an instance method, target is an object. | ||
if (typeof target === 'object') { | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
prototype = target; | ||
} | ||
else { | ||
throw new Error('@native can only be used with instance class methods'); | ||
} | ||
/* eslint-enable */ | ||
// The class has to implement the instance method | ||
// getRegisteredPluginName(). | ||
if (typeof prototype.getRegisteredPluginName === 'function') { | ||
const pluginName = prototype.getRegisteredPluginName(); | ||
descriptor.value = wrappedFunction(pluginName, methodName, returnType, originalMethod); | ||
} | ||
else { | ||
throw new Error('Classes that use @native must implement the method getRegisteredPluginName() => string'); | ||
} | ||
} | ||
else { | ||
throw new Error('@native can only be used with instance class methods'); | ||
} | ||
return descriptor; | ||
}; | ||
} | ||
//# sourceMappingURL=native-decorator.js.map |
{ | ||
"name": "@aparajita/capacitor-native-decorator", | ||
"version": "1.1.1", | ||
"description": "Decorator for Capacitor plugins that allows painless JavaScript<->native integration", | ||
"main": "dist/esm/index.js", | ||
"version": "2.0.0", | ||
"description": "Decorator for Capacitor plugins that allows painless TypeScript<->native integration", | ||
"main": "dist/plugin.cjs.js", | ||
"module": "dist/esm/index.js", | ||
"types": "dist/esm/index.d.ts", | ||
"unpkg": "dist/plugin.js", | ||
"engines": { | ||
"node": ">=16.15.1" | ||
}, | ||
"bin": { | ||
@@ -13,11 +19,2 @@ "make-ios-plugin": "dist/cli/make-ios-plugin.js" | ||
], | ||
"types": "dist/esm/index.d.ts", | ||
"scripts": { | ||
"clean": "rimraf dist", | ||
"build": "npm run clean && tsc && tsc --build tsconfig-cli.json", | ||
"watch": "nodemon -w ./src -w ./cli -w tsconfig.json -w tsconfig-cli.json --exec 'npm run build --silent' -e ts", | ||
"release": "standard-version", | ||
"lint": "prettier --check \"**/*.{ts,js}\"", | ||
"lint.fix": "prettier --write --check \"**/*.{ts,js}\"" | ||
}, | ||
"repository": { | ||
@@ -38,14 +35,41 @@ "type": "git", | ||
"dependencies": { | ||
"@capacitor/core": "^2.4.7" | ||
"@capacitor/core": "^3.6.0", | ||
"tslib": "^2.4.0" | ||
}, | ||
"devDependencies": { | ||
"@ionic/prettier-config": "^1.0.1", | ||
"@types/node": "^14.14.36", | ||
"nodemon": "^2.0.7", | ||
"prettier": "^2.2.1", | ||
"@aparajita/eslint-config-base": "^1.1.2", | ||
"@aparajita/prettier-config": "^1.0.0", | ||
"@types/node": "^18.0.0", | ||
"@typescript-eslint/eslint-plugin": "^5.30.2", | ||
"@typescript-eslint/parser": "^5.30.2", | ||
"commit-and-tag-version": "^10.0.1", | ||
"eslint": "^8.18.0", | ||
"eslint-config-prettier": "^8.5.0", | ||
"eslint-config-standard": "^17.0.0", | ||
"eslint-import-resolver-typescript": "^3.1.4", | ||
"eslint-plugin-import": "^2.26.0", | ||
"eslint-plugin-n": "^15.2.3", | ||
"eslint-plugin-prettier": "^4.2.1", | ||
"eslint-plugin-promise": "^6.0.0", | ||
"nodemon": "^2.0.18", | ||
"prettier": "^2.7.1", | ||
"rimraf": "^3.0.2", | ||
"standard-version": "^9.1.1", | ||
"typescript": "^4.2.3" | ||
"rollup": "^2.75.7", | ||
"typescript": "^4.7.4" | ||
}, | ||
"prettier": "@ionic/prettier-config" | ||
} | ||
"scripts": { | ||
"clean": "rimraf dist", | ||
"build": "pnpm check && pnpm build.only", | ||
"build.only": "pnpm clean && tsc && tsc --build tsconfig-cli.json && rollup -c rollup.config.js", | ||
"watch": "nodemon -w ./src -w ./cli -w tsconfig.json -w tsconfig-cli.json --exec 'pnpm run build.only --silent' -e ts", | ||
"lint": "eslint --ext .js,.cjs,.mjs,.ts .", | ||
"lint.fix": "pnpm lint --fix", | ||
"check": "pnpm lint && pnpm typecheck && pnpm prettier && echo '✅ All good!'", | ||
"check.fix": "pnpm lint.fix && pnpm typecheck && pnpm prettier.fix && echo '✅ All good!'", | ||
"prettier": "prettier --check .", | ||
"prettier.fix": "pnpm prettier --write", | ||
"typecheck": "tsc --noEmit", | ||
"release": "pnpm build && commit-and-tag-version", | ||
"push": "git push --follow-tags origin main" | ||
} | ||
} |
410
README.md
# capacitor-native-decorator | ||
This package adds a **@native** decorator to TypeScript, which fundamentally changes the way we write and call Capacitor plugins. | ||
This package adds a `@native` decorator to TypeScript, which fundamentally changes the way we write and call Capacitor plugins. | ||
**NOTE:** This package has only been tested with Capacitor 2. | ||
👉 **This package only works with Capacitor 3.** | ||
@@ -11,3 +11,2 @@ [Motivation](#motivation)<br> | ||
[Usage](#usage)<br> | ||
[Caveats](#caveats)<br> | ||
[Example](#example) | ||
@@ -17,107 +16,220 @@ | ||
Have you ever wished you could receive a value from a plugin without needing to deconstruct and object? | ||
In the process of developing Capacitor plugins, I built up a big wish list: | ||
Have you ever wished your plugins could leverage the full power of TypeScript code when running native? | ||
- I wish I only had to make one TypeScript version of my plugins for all platforms. | ||
Have you ever wished you could manage state and add TypeScript convenience methods in your plugin classes without having it disappear when running native? | ||
- I wish I could pass values to a plugin without constructing an object. | ||
Have you ever wished you didn't have to maintain the `ios/Plugin/Plugin.m` file manually? | ||
- I wish I could receive a single value from a plugin without needing to deconstruct an object. | ||
### The mysterious `platforms` array | ||
- I wish my plugins could leverage the full power of TypeScript code when running native. | ||
By default, a newly created plugin contains a single value in the `platforms` array passed to the superclass constructor: `'web'`. This tells Capacitor that *all* of the code in the plugin class will *completely* disappear on native platforms. On a native platform, calls to any instance methods that exist in both the TypeScript plugin class and the native plugin will automatically be routed to the native code. Calls to any other instance methods will silently disappear into the void — which wasn’t quite what I expected. | ||
- I wish I could manage state and add TypeScript convenience methods in my plugin classes without having it disappear when running native. | ||
Haven’t you ever wished you could keep some code and state in the TypeScript class and some in the native plugin? You may think that the solution is to add the other platforms to the `platforms` array. So you set the `platforms` array to `['web', 'ios', 'android']`, and run your code on iOS or Android... and none of your native code gets called. That's because the TypeScript code is kept, but no automatic mapping of TypeScript methods to native methods happens. | ||
- I wish I didn’t have to maintain the `ios/Plugin/Plugin.m` file manually. | ||
What many developers may not know is that Capacitor does provide a way to call a plugin method from TypeScript: `Capacitor.toNative()`. So it is *technically* possible to keep your TypeScript code and call native methods, but *practically* speaking it almost isn’t, because the interface of `toNative()` is cumbersome to say the least, and requires huge amounts of boilerplate code. | ||
Thus was born `@native`. With `@native`, I — and you — get all of these things and more! | ||
**@native** solves all these problems, and much more. | ||
### Where did my code go? | ||
On native platforms, calls to any instance methods that exist in both the TypeScript plugin class and the native plugin will automatically be routed to the native code. Calls to any other instance methods will silently disappear into the void — which wasn’t quite what I expected when I first encountered this. | ||
Have you ever wished you could keep some code and state in the TypeScript class and some in the native plugin? You may think that the solution is to register your code on the other platforms, but when you run your code on iOS or Android, none of your native code gets called. That’s because the TypeScript code is kept, but no automatic mapping of TypeScript methods to native methods happens. | ||
What you may not know is that Capacitor **does** provide a way to call a plugin method from TypeScript: `Capacitor.nativeCallback()` and `Capacitor.nativePromise()`. So it is _technically_ possible to keep your TypeScript code and call native methods, but _practically_ speaking it isn’t, because the interface of those methods is cumbersome and requires a lot of boilerplate code. | ||
`@native` solves all these problems, and much more. | ||
## Features | ||
**@native** is a TypeScript method decorator. It’s quite simple to use. You just add it before an instance method declaration, like this: | ||
`@native` is a TypeScript method decorator. It’s quite simple to use. You just add it before an instance method declaration, like this: | ||
`definitions.ts` | ||
```typescript | ||
import { native } from '@aparajita/capacitor-native-decorator'; | ||
import { DecoratedNativePlugin } from '@aparajita/capacitor-native-decorator' | ||
export type DataType = string | number | boolean | Array<any> | Object | null | Date; | ||
// Always extend DecoratedNativePlugin, this ensures you implement | ||
// getRegisteredPluginName(), which @native needs at runtime. | ||
export interface AwesomePlugin extends DecoratedNativePlugin { | ||
setItem: (key: string, data: string | number) => Promise<void> | ||
getItem: (key: string) => Promise<string> | ||
} | ||
``` | ||
export class MyPlugin | ||
extends WebPlugin | ||
implements WSBiometricAuthPlugin { | ||
private _storageCount = 0; | ||
constructor() { | ||
super({ | ||
name: 'MyPlugin', | ||
platforms: ['web', 'ios', 'android'] | ||
}); | ||
} | ||
`web.ts` | ||
```typescript | ||
import { native, PluginReturnType } from '@aparajita/capacitor-native-decorator' | ||
import { AwesomePlugin } from './definitions' | ||
import { PluginCallback } from '@capacitor/core' | ||
export class Awesome extends WebPlugin implements AwesomePlugin { | ||
private _storageCount = 0 | ||
// This is usable even on native platforms! | ||
get storageCount() { | ||
return this._storageCount; | ||
get storageCount(): number { | ||
return this._storageCount | ||
} | ||
// 👇🏼 Here's where the magic happens. Be sure to include the (). | ||
@native() | ||
private setStringItem(options: { key: string, data: string }): Promise<void> { | ||
// IMPORTANT: This has to be defined because at runtime @native needs your | ||
// *registered* plugin name, and when your code is minimized the actual | ||
// name will be different. | ||
getRegisteredPluginName(): string { | ||
return 'Awesome' | ||
} | ||
/* | ||
👇🏼 Here's where the 🪄magic happens. | ||
Like any method that will be native, it has to be async and should | ||
return a Promise. By default, @native assumes the method will return | ||
a Promise with data. If it returns nothing, we indicate that by passing | ||
PluginReturnType.none to the decorator. | ||
*/ | ||
@native(PluginReturnType.none) | ||
private async setStringItem(options: { | ||
key: string | ||
value: string | ||
}): Promise<void> { | ||
// Your web implementation goes here. On native platforms | ||
// this code won't be used, but the method's interface is the same! | ||
localStorage.setItem(key, data) | ||
return Promise.resolve() | ||
} | ||
// More magic! | ||
// No need to specify the return type, by default it's a promise with data. | ||
// Note that even though this is a native call, it is returning a bare string, | ||
// not an object! @native automatically unwraps single values returned by | ||
// native code. | ||
@native() | ||
private getStringItem({ key: string }): Promise<string> { | ||
private async getStringItem({ key: string }): Promise<string> { | ||
// Web implementation goes here. Same interface on native platforms. | ||
return Promise.resolve(localStorage.getItem(key)) | ||
} | ||
setItem(key: string, data: DataType): Promise<void> { | ||
// I'll leave convertToString() up to you :) | ||
this._storageCount += 1; | ||
this.setStringItem({ key, data: convertToString(data) }); | ||
return Promise.resolve(); | ||
// We can also use callback methods with @native! Be sure to specify | ||
// it as such with the return type. | ||
@native(PluginReturnType.callback) | ||
async getTime(callback: PluginCallback): Promise<string> { | ||
window.setTimeout(() => { | ||
// PluginCallback expects to be passed a data object | ||
callback({ time: new Date().toString() }) | ||
}) | ||
// On the web, the pluginCallId can be anything, it isn't used | ||
return Promise.resolve('getTime') | ||
} | ||
getItem(key: string): Promise<DataType> { | ||
// I'll leave convertFromString() up to you :) | ||
return Promise.resolve(convertFromString(this.getStringItem({ key }))); | ||
// This is the method you will call to set an item. More natural | ||
// because you don't have to construct an object. Plus we can implement | ||
// code in the TypeScript world and still access the native code. | ||
async setItem(key: string, value: string | number): Promise<void> { | ||
this._storageCount += 1 | ||
return this.setStringItem({ key, data: String(value) }) | ||
} | ||
} | ||
// And in a file that uses MyPlugin... | ||
async function storeCount(count: number) { | ||
await plugin.setItem('count', count); | ||
// This is the method you will call to get an item. More natural | ||
// because you don't have to construct an object. | ||
async getItem(key: string): Promise<string> { | ||
return this.getStringItem({ key }) | ||
} | ||
} | ||
``` | ||
async function retrieveCount(): number { | ||
// getItem() returns a bare DataType, **not** an object | ||
const count = await plugin.getItem('count'); | ||
// Use a type guard so we can return it as a number | ||
if (typeof count === 'number') { | ||
return count; | ||
`Plugin.m` | ||
Note that `make-ios-plugin` will generate this for you! | ||
```swift | ||
#import <Foundation/Foundation.h> | ||
#import <Capacitor/Capacitor.h> | ||
// Generated by @aparajita/capacitor-native-decorator/make-ios-plugin | ||
CAP_PLUGIN(Awesome, "Awesome", | ||
CAP_PLUGIN_METHOD(setStringItem, CAPPluginReturnNone); | ||
CAP_PLUGIN_METHOD(getStringItem, CAPPluginReturnPromise); | ||
CAP_PLUGIN_METHOD(getTime, CAPPluginReturnCallback); | ||
) | ||
``` | ||
`Plugin.swift` | ||
```swift | ||
@objc(BiometricAuth) | ||
public class Awesome: CAPPlugin { | ||
@objc func setStringItem(_ call: CAPPluginCall) { | ||
// storeValue is defined by you somewhere | ||
storeValue(call.getString("key"), call.getString("value)) | ||
call.resolve() | ||
} | ||
// Oops, this shouldn't happen... | ||
@objc func getStringItem(_ call: CAPPluginCall) { | ||
var value = "" | ||
if let key = call.getString("key") { | ||
// getValue is defined by you somewhere | ||
value = getValue(key) | ||
} | ||
call.resolve(["value": value]) | ||
} | ||
@objc func getTime(_ call: CAPPluginCall) { | ||
// This has to be done for callback methods | ||
// so you can repeatedly resolve(). | ||
call.keepAlive = true | ||
DispatchQueue.main.async { | ||
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in | ||
call.resolve([ | ||
"time": Date().description | ||
]) | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
And in a file that uses Awesome... | ||
```ts | ||
import { Awesome } from 'myplugin' | ||
async function storeCount(count: number): Promise<void> { | ||
await Awesome.setItem('count', count) | ||
console.log(`${Awesome.storageCount} item(s) stored`) | ||
} | ||
async function retrieveCount(): Promise<number> { | ||
const count = await Awesome.getItem('count') | ||
return Number(count) | ||
} | ||
async function startClock(): Promise<string> { | ||
return Awesome.getTime(({ time }) => { | ||
console.log(time) | ||
}) | ||
} | ||
``` | ||
There are quite a number of interesting points to make about this code. | ||
**Mix and match TypeScript and native methods** | ||
When you add the `@native` decorator to a method, it does all of the hard work of calling `Capacitor.toNative()` and returning its result for you. Anything marked `@native` will automatically route to native code when called from the TS/JS world, while still allowing you to keep all of your lovely TypeScript plugin code. | ||
### Mix and match TypeScript and native methods | ||
For example, in the above code, the public API to the plugin is pure TypeScript code, which then calls private methods that will execute native code. (NOTE: `@native` methods do not have to be private, they can just as easily be public.) This is *incredibly* powerful. Why? Because now the API to your plugin can be changed and extended without having to change the native code. | ||
When you add the `@native` decorator to a method, it does all of the hard work of calling `Capacitor.nativePromise()` or `Capacitor.nativeCallback()` and returning its result for you. Anything marked `@native` will automatically route to native code when called from the TS/JS world, while still allowing you to keep all of your lovely TypeScript plugin code. | ||
For example, in the above code, some of the public API to the plugin is pure TypeScript code, which then calls private methods that will execute native code. This is _incredibly_ powerful. Why? Because now the API to your plugin can be changed and extended without having to change the native code. | ||
As in the example above, you can modify the parameters going into the native method and the result coming back. Or you can add or remove to either. Go wild! Anything you can do in TypeScript, you can now do with native plugins. | ||
Because you have free access to TypeScript when running native, you can let your native code focus on things only it *can* do, or on things it does best. Lets face it — it's way easier to do most stuff in TypeScript than in Swift or Java. And anything native code does has to be duplicated across iOS and Android in two different languages and SDKs. So having the ability to move code out of native and into TypeScript is a huge win. | ||
Because you have free access to TypeScript when running native, you can let your native code focus on things only it can do, or on things it does best. Lets face it — it's way easier to do most stuff in TypeScript than in Swift or Java. And anything native code does has to be duplicated across iOS and Android in two different languages and SDKs. So having the ability to move code out of native and into TypeScript is a huge win. | ||
**Natural calling syntax** | ||
Looking at the code above, you may have noticed that non-object parameters are being returned from a method that is marked native. You may be scratching your head and thinking, “Wait, how is that possible? I thought we have to return an object, even for a single value.” | ||
### Natural calling syntax | ||
The `@native` decorator makes this possible. If the object returned by a native method contains a single property, the call to the method resolves to the bare value of that property. In any other case, the call resolves to the returned object. | ||
Looking at the code above, you may have noticed that the `@native getStringItem()` returns `Promise<string>` and not `Promise<SomeObjectWithAString>`. You may be scratching your head and thinking, “Wait, how is that possible? I thought we have to return an object, even for a single value.” | ||
The `@native` decorator makes this possible. If the object returned by a native method contains a single property, `@native` unwraps the value and the call to the method resolves to the bare value of that property. In any other case, the call resolves to the returned object. | ||
For example: | ||
@@ -127,23 +239,44 @@ | ||
// If the native implementation of a @native method returns this... | ||
{ value: "foobar" } // The property name is irrelevant, it can be anything | ||
{ | ||
value: 'foobar' | ||
} // The property name is irrelevant, it can be anything | ||
// a call to that method would resolve to the bare string: | ||
"foobar" | ||
;('foobar') | ||
``` | ||
**Plugin.m generation** | ||
### Plugin.m generation | ||
When you install this package, a `make-ios-plugin` binary is installed. Executing that binary parses the `dist/plugin.js` file generated by `tsc` and automatically generates the `ios/Plugin/Plugin.m` file necessary to make your native iOS methods callable. Whenever you add, remove or rename `@native` methods, `Plugin.m` will stay in sync, which means one less thing to maintain (and get wrong). Woo hoo! 🎉 | ||
### Are decorators safe to use? | ||
In short, absolutely. | ||
The TypeScript documentation says this about decorators: “Decorators are an experimental feature that may change in future releases.” | ||
Decorators may _**change**_, but there is no chance they are going away, because they are heavily used by a little framework called Angular made by a little company called Google. In fact, the story goes that Microsoft implemented decorators in TypeScript because Google wanted them for Angular and threatened to fork TypeScript in order to get them. | ||
In addition, decorators are currently a [Stage 3 proposal](https://github.com/tc39/proposal-decorators) for the JavaScript language, and the proposed implementation will allow this plugin to continue working with some minor changes. Having reached Stage 3, it’s only a matter of time (historically speaking) until decorators become part of JavaScript. | ||
So don’t be scared off by the “experimental” label on decorators. The experiment was a success. | ||
### But I’m loading web code I don’t need! | ||
On really, really cheap phones with limited memory and CPU, every extra byte of JavaScript incurs a cost. But here’s the thing: | ||
- In a production app, your JavaScript/TypeScript code is minimized to a fraction of its original size. | ||
- If an app is going to crash or slow down because of a few hundred extra bytes in a plugin, then you probably cannot afford to add any other functionality — and thus code — to your app either. | ||
So unless your app has to run on extremely memory-challenged phones, the advantages you get from `@native` are well worth any extra overhead. | ||
## Installation | ||
```sh | ||
pnpm install @aparajita/capacitor-native-decorator tslib # 'pnpm add' also works | ||
npm install @aparajita/capacitor-native-decorator tslib | ||
yarn add @aparajita/capacitor-native-decorator tslib | ||
```shell | ||
% pnpm add @aparajita/capacitor-native-decorator tslib | ||
``` | ||
`tslib` contains the code that implements decorators. It is tree shaken by `rollup` during the build, so adds very little code. | ||
Not using [pnpm](https://pnpm.io/)? You owe it to yourself to give it a try. It’s actually the official package manager used by the Vue team. It’s faster, better with monorepos, and uses _way, way_ less disk space than the alternatives. | ||
Not using [pnpm](https://pnpm.js.org/)? You owe it to yourself to give it a try. It’s faster, better with monorepos, and uses *way, way* less disk space than the alternatives. | ||
## Usage | ||
@@ -153,21 +286,44 @@ | ||
##### 1. Modify `platforms` | ||
#### 1. Extend your interface from `DecoratedNativePlugin` | ||
Change the constructor of your plugin to look like this (where `MyPlugin` is your plugin’s name): | ||
At runtime `@native` needs to know the **registered** name of your plugin. This cannot be determined from the **declared** name, because when your code is minimized the names are changed and do not match the registered name. | ||
`@native` relies on you implementing a `getRegisteredPluginName` method that returns the registered name. To ensure you don’t forget to implement this method and implement it with the proper signature, you shoud extend your plugin interface from `DecoratedNativePlugin`: | ||
```typescript | ||
constructor() { | ||
super({ | ||
name: 'MyPlugin', | ||
platforms: ['web', 'ios', 'android'] | ||
}); | ||
// Your custom code here | ||
import { DecoratedNativePlugin } from '@aparajita/capacitor-native-decorator' | ||
export interface AwesomePlugin extends DecoratedNativePlugin { | ||
// your methods here | ||
} | ||
``` | ||
##### 2. Modify `tsconfig.js` | ||
#### 2. Modify `registerPlugin` | ||
Add the following to your `tsconfig.js` if they are not already there: | ||
Change the `index.ts` of your plugin to look like this (where `Awesome` is your plugin’s name): | ||
```typescript | ||
import { registerPlugin } from '@capacitor/core' | ||
import type { AwesomePlugin } from './definitions' | ||
import { Awesome } from './web' | ||
// Because we are using the @native decorator, we have one version | ||
// of the TS code to rule them all, and there is no need to lazy load. | ||
// And our code is available on all platforms. 😁 | ||
const plugin = new Awesome() | ||
const awesome = registerPlugin<AwesomePlugin>('Awesome', { | ||
web: plugin, | ||
ios: plugin, | ||
android: plugin | ||
}) | ||
export * from './definitions' | ||
export { awesome as Awesome } | ||
``` | ||
#### 3. Modify `tsconfig.js` | ||
Add the following to your `tsconfig.js` if it is not already there: | ||
```json | ||
@@ -182,46 +338,50 @@ { | ||
##### 3. Modify `rollup.config.js` | ||
#### 4. Modify `rollup.config.js` | ||
Your `rollup.config.js` should look something like this: | ||
You need to tell `rollup` about `@native` by adding three items: | ||
```js | ||
import commonjs from '@rollup/plugin-commonjs'; | ||
import nodeResolve from '@rollup/plugin-node-resolve'; | ||
export default { | ||
input: 'dist/esm/index.js', | ||
output: { | ||
file: 'dist/plugin.js', | ||
format: 'iife', | ||
name: 'MyGreatPlugin', | ||
globals: { | ||
'@capacitor/core': 'capacitorExports', | ||
output: [ | ||
{ | ||
file: 'dist/plugin.js', | ||
format: 'iife', | ||
name: 'capacitorAwesome', // My plugin name | ||
globals: { | ||
'@capacitor/core': 'capacitorExports', | ||
// ===> You need to add this <=== | ||
'@aparajita/capacitor-native-decorator': 'capacitorNativeDecorator' | ||
// =============================== | ||
}, | ||
sourcemap: true, | ||
inlineDynamicImports: true | ||
}, | ||
sourcemap: true, | ||
}, | ||
plugins: [ | ||
nodeResolve({ | ||
// allowlist of dependencies to bundle in | ||
// @see https://github.com/rollup/plugins/tree/master/packages/node-resolve#resolveonly | ||
resolveOnly: [ | ||
'tslib', | ||
'@aparajita/capacitor-native-decorator' | ||
], | ||
}), | ||
commonjs(), | ||
{ | ||
file: 'dist/plugin.cjs.js', | ||
format: 'cjs', | ||
sourcemap: true, | ||
inlineDynamicImports: true | ||
} | ||
], | ||
}; | ||
// ===> You need to add the second item here <=== | ||
external: ['@capacitor/core', '@aparajita/capacitor-native-decorator'], | ||
// ===> You need to add this <=== | ||
context: 'window' | ||
} | ||
``` | ||
The important thing is to include `'tslib'` and `'@aparajita/capacitor-native-decorator'` in the `resolveOnly` array. | ||
##### 5. Call `make-ios-plugin` in the `build` script | ||
##### 4. Call `make-ios-plugin` in the `build` script | ||
Somewhere in your `package.json` scripts, you will want to call `make-ios-plugin` to automatically create the `Plugin.m` file for iOS. For example: | ||
Add ` && make-ios-plugin` to the `build` script in `package.json`. It will look something like this: | ||
``` | ||
"build": "npm run clean && tsc && rollup -c rollup.config.js && make-ios-plugin" | ||
"build": "pnpm run clean && tsc && rollup -c rollup.config.js && pnpm make" | ||
"make": "make-ios-plugin" | ||
``` | ||
##### 5. Add `@native() ` to your native methods | ||
##### 6. Add `@native() ` to your native methods | ||
@@ -234,15 +394,9 @@ Import the `native` decorator function: | ||
Now you can add the `@native()` decorator above the TypeScript implementation of any methods that have a native implementation, and you’re all set! | ||
Now you can add the `@native()` decorator above the TypeScript implementation of any methods that have a native implementation. | ||
## Caveats | ||
Be sure to pass the return type of your methods to `@native()` if they return `Promise<void>` or they take a callback function. | ||
There are several issues related to usage of `@native` that you should be aware of before deciding if it’s right for you. | ||
- Decorators are experimental, and at some point their current implementation will be replaced with an official decorator implementation in ECMAScript. However, that does not mean decorators are a dead end. Even if decorators are completely dropped from TypeScript or their syntax changes dramatically when they are officially adopted, a custom compiler can be used to support the current syntax. | ||
- On inexpensive phones with limited memory and CPU, every extra byte of JavaScript incurs a cost that is greater than the equivalent native code. If you are targeting such phones, you may want to think twice before using `@native`. | ||
## Example | ||
A complete working example of `@native` can be found in the [capacitor-secure-storage plugin](https://github.com/aparajita/capacitor-secure-storage). There you can find all of the features of `@native` used: | ||
A complete working example of `@native` can be found in the [capacitor-secure-storage plugin](https://github.com/aparajita/capacitor-secure-storage). There you can find almost all of the features of `@native` used: | ||
@@ -254,2 +408,2 @@ - Returning non-object values | ||
I hope you find it useful. | ||
I hope you find it useful! |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
61129
18
475
403
2
19
1
+ Addedtslib@^2.4.0
+ Added@capacitor/core@3.9.0(transitive)
+ Addedtslib@2.8.1(transitive)
- Removed@capacitor/core@2.5.0(transitive)
- Removedtslib@1.14.1(transitive)
Updated@capacitor/core@^3.6.0