diff --git a/android/capacitor/src/main/java/com/getcapacitor/JSExport.java b/android/capacitor/src/main/java/com/getcapacitor/JSExport.java index 3606fc411e..4ea4160739 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/JSExport.java +++ b/android/capacitor/src/main/java/com/getcapacitor/JSExport.java @@ -8,6 +8,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; public class JSExport { @@ -40,6 +43,7 @@ public static String getCordovaPluginsFileJS(Context context) { public static String getPluginJS(Collection plugins) { List lines = new ArrayList<>(); + JSONArray pluginArray = new JSONArray(); lines.add("// Begin: Capacitor Plugin JS"); @@ -57,21 +61,21 @@ public static String getPluginJS(Collection plugins) { "', eventName, callback);\n" + "}" ); - Collection methods = plugin.getMethods(); - for (PluginMethodHandle method : methods) { if (method.getName().equals("addListener") || method.getName().equals("removeListener")) { // Don't export add/remove listener, we do that automatically above as they are "special snowflakes" continue; } - lines.add(generateMethodJS(plugin, method)); } + lines.add("})(window);\n"); + + pluginArray.put(createPluginHeader(plugin)); } - return TextUtils.join("\n", lines); + return TextUtils.join("\n", lines) + "\nwindow.Capacitor.PluginHeaders = " + pluginArray.toString() + ";"; } public static String getCordovaPluginJS(Context context) { @@ -95,6 +99,40 @@ public static String getFilesContent(Context context, String path) { return builder.toString(); } + private static JSONObject createPluginHeader(PluginHandle plugin) { + JSONObject pluginObj = new JSONObject(); + Collection methods = plugin.getMethods(); + try { + String id = plugin.getId(); + JSONArray methodArray = new JSONArray(); + pluginObj.put("name", id); + + for (PluginMethodHandle method : methods) { + methodArray.put(createPluginMethodHeader(method)); + } + + pluginObj.put("methods", methodArray); + } catch (JSONException e) { + // ignore + } + return pluginObj; + } + + private static JSONObject createPluginMethodHeader(PluginMethodHandle method) { + JSONObject methodObj = new JSONObject(); + + try { + methodObj.put("name", method.getName()); + if (!method.getReturnType().equals(PluginMethod.RETURN_NONE)) { + methodObj.put("rtype", method.getReturnType()); + } + } catch (JSONException e) { + // ignore + } + + return methodObj; + } + private static String generateMethodJS(PluginHandle plugin, PluginMethodHandle method) { List lines = new ArrayList<>(); diff --git a/android/capacitor/src/main/java/com/getcapacitor/JSInjector.java b/android/capacitor/src/main/java/com/getcapacitor/JSInjector.java index f835f789ba..00b1d0a807 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/JSInjector.java +++ b/android/capacitor/src/main/java/com/getcapacitor/JSInjector.java @@ -20,10 +20,6 @@ class JSInjector { private String cordovaPluginsFileJS; private String localUrlJS; - public JSInjector(String globalJS, String pluginJS) { - this(globalJS, pluginJS, ""/* cordovaJS */, ""/* cordovaPluginsJS */, ""/* cordovaPluginsFileJS */, ""/* localUrlJS */); - } - public JSInjector( String globalJS, String pluginJS, diff --git a/core/src/definitions-internal.ts b/core/src/definitions-internal.ts index 1e5c28bd30..692dde3e7a 100644 --- a/core/src/definitions-internal.ts +++ b/core/src/definitions-internal.ts @@ -5,6 +5,16 @@ import type { PluginResultError, } from './definitions'; +export interface PluginHeaderMethod { + readonly name: string; + readonly rtype?: 'promise' | 'callback'; +} + +export interface PluginHeader { + readonly name: string; + readonly methods: readonly PluginHeaderMethod[]; +} + /** * Has all instance properties that are available and used * by the native layer. The "Capacitor" interface it extends @@ -23,6 +33,8 @@ export interface CapacitorInstance extends CapacitorGlobal { }; }; + PluginHeaders?: readonly PluginHeader[]; + /** * Low-level API to send data to the native layer. * Prefer using `nativeCallback()` or `nativePromise()` instead. diff --git a/core/src/runtime.ts b/core/src/runtime.ts index 0b34b2beba..758671f9a7 100644 --- a/core/src/runtime.ts +++ b/core/src/runtime.ts @@ -2,6 +2,7 @@ import { getPlatformId, initBridge } from './bridge'; import type { CapacitorGlobal, PluginImplementations } from './definitions'; import type { CapacitorInstance, + PluginHeader, WindowCapacitor, } from './definitions-internal'; import { initEvents } from './events'; @@ -25,8 +26,24 @@ export const createCapacitor = (win: WindowCapacitor): CapacitorInstance => { const isNativePlatform = () => getPlatformId(win) !== 'web'; - const isPluginAvailable = (pluginName: string) => - Object.prototype.hasOwnProperty.call(Plugins, pluginName); + const isPluginAvailable = (pluginName: string): boolean => { + const plugin = registeredPlugins.get(pluginName); + + if (plugin && getPlatform() in plugin.implementations) { + // JS implementation available for the current platform. + return true; + } + + if (getPluginHeader(pluginName)) { + // Native implementation available. + return true; + } + + return false; + }; + + const getPluginHeader = (pluginName: string): PluginHeader | undefined => + cap.PluginHeaders?.find(h => h.name === pluginName); const convertFileSrc = (filePath: string) => convertFileSrcServerUrl(webviewServerUrl, filePath); @@ -59,8 +76,13 @@ export const createCapacitor = (win: WindowCapacitor): CapacitorInstance => { ); }; + interface RegisteredPlugin { + proxy: any; + implementations: PluginImplementations; + } + // ensure we do not double proxy the same plugin - const registeredPlugins = new Map(); + const registeredPlugins = new Map(); const registerPlugin = ( pluginName: string, @@ -68,7 +90,7 @@ export const createCapacitor = (win: WindowCapacitor): CapacitorInstance => { ): any => { const registeredPlugin = registeredPlugins.get(pluginName); if (registeredPlugin) { - return registeredPlugin; + return registeredPlugin.proxy; } const nativePluginImpl = Plugins[pluginName]; @@ -76,7 +98,7 @@ export const createCapacitor = (win: WindowCapacitor): CapacitorInstance => { // the native implementation is already on the global // return a proxy that'll also handle any missing methods // convert the Capacitor.Plugins.PLUGIN into a proxy and return it - const nativePluginProxy = (Plugins[pluginName] = new Proxy( + const nativePluginProxy = (Plugins[pluginName] = new Proxy( {}, { get(_, prop) { @@ -101,7 +123,10 @@ export const createCapacitor = (win: WindowCapacitor): CapacitorInstance => { }, }, )); - registeredPlugins.set(pluginName, nativePluginProxy); + registeredPlugins.set(pluginName, { + implementations: impls, + proxy: nativePluginProxy, + }); return nativePluginProxy; } @@ -110,7 +135,7 @@ export const createCapacitor = (win: WindowCapacitor): CapacitorInstance => { // there isn't a native implementation already on the global // create a Proxy which is used to lazy load implementations - const pluginProxy = (Plugins[pluginName] = new Proxy( + const pluginProxy = (Plugins[pluginName] = new Proxy( {}, { get(_, prop) { @@ -229,7 +254,10 @@ export const createCapacitor = (win: WindowCapacitor): CapacitorInstance => { }, }, )); - registeredPlugins.set(pluginName, pluginProxy); + registeredPlugins.set(pluginName, { + implementations: impls, + proxy: pluginProxy, + }); return pluginProxy; }; diff --git a/core/src/tests/runtime.spec.ts b/core/src/tests/runtime.spec.ts index a2d53cc1ba..4eebb5a3af 100644 --- a/core/src/tests/runtime.spec.ts +++ b/core/src/tests/runtime.spec.ts @@ -22,6 +22,7 @@ describe('runtime', () => { it('used existing window.Capacitor.Plugins', () => { win.Capacitor = { Plugins: { Awesome: {} }, + PluginHeaders: [{ name: 'Awesome', methods: [] }], } as any; cap = createCapacitor(win); expect(cap.isPluginAvailable('Awesome')).toBe(true); diff --git a/ios/Capacitor/Capacitor/JSExport.swift b/ios/Capacitor/Capacitor/JSExport.swift index 6b1a06eea7..be348ac7de 100644 --- a/ios/Capacitor/Capacitor/JSExport.swift +++ b/ios/Capacitor/Capacitor/JSExport.swift @@ -1,3 +1,13 @@ +internal struct PluginHeaderMethod: Codable { + let name: String + let rtype: String? +} + +internal struct PluginHeader: Codable { + let name: String + let methods: [PluginHeaderMethod] +} + /** * PluginExport handles defining JS APIs that map to registered * plugins and are responsible for proxying calls to our bridge. @@ -56,12 +66,38 @@ internal class JSExport { })(window); """) + if let data = try? JSONEncoder().encode(createPluginHeader(pluginClassName: pluginClassName, pluginType: pluginType)), let header = String(data: data, encoding: .utf8) { + lines.append(""" + (function(w) { + var a = (w.Capacitor = w.Capacitor || {}); + var h = (a.PluginHeaders = a.PluginHeaders || []); + h.push(\(header)); + })(window); + """) + } + let js = lines.joined(separator: "\n") let userScript = WKUserScript(source: js, injectionTime: .atDocumentStart, forMainFrameOnly: true) userContentController.addUserScript(userScript) } + private static func createPluginHeader(pluginClassName: String, pluginType: CAPPlugin.Type) -> PluginHeader? { + if let bridgeType = pluginType as? CAPBridgedPlugin.Type, let methods = bridgeType.pluginMethods() as? [CAPPluginMethod] { + return PluginHeader(name: pluginClassName, methods: methods.map { createPluginHeaderMethod(method: $0) }) + } + + return nil + } + + private static func createPluginHeaderMethod(method: CAPPluginMethod) -> PluginHeaderMethod { + var rtype = method.returnType + if rtype == "none" { + rtype = nil + } + return PluginHeaderMethod(name: method.name, rtype: rtype) + } + private static func generateMethod(pluginClassName: String, method: CAPPluginMethod) -> String { let methodName = method.name! let returnType = method.returnType!