Skip to content

Commit

Permalink
fix: address bug in isPluginAvailable() for web and native (#4114)
Browse files Browse the repository at this point in the history
  • Loading branch information
imhoffd committed Jan 28, 2021
1 parent 4cbae41 commit 2fbd954
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 16 deletions.
46 changes: 42 additions & 4 deletions android/capacitor/src/main/java/com/getcapacitor/JSExport.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -40,6 +43,7 @@ public static String getCordovaPluginsFileJS(Context context) {

public static String getPluginJS(Collection<PluginHandle> plugins) {
List<String> lines = new ArrayList<>();
JSONArray pluginArray = new JSONArray();

lines.add("// Begin: Capacitor Plugin JS");

Expand All @@ -57,21 +61,21 @@ public static String getPluginJS(Collection<PluginHandle> plugins) {
"', eventName, callback);\n" +
"}"
);

Collection<PluginMethodHandle> 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) {
Expand All @@ -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<PluginMethodHandle> 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<String> lines = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions core/src/definitions-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
44 changes: 36 additions & 8 deletions core/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -59,24 +76,29 @@ 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<string, any>();
const registeredPlugins = new Map<string, RegisteredPlugin>();

const registerPlugin = (
pluginName: string,
impls: PluginImplementations = {},
): any => {
const registeredPlugin = registeredPlugins.get(pluginName);
if (registeredPlugin) {
return registeredPlugin;
return registeredPlugin.proxy;
}

const nativePluginImpl = Plugins[pluginName];
if (nativePluginImpl) {
// 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<any>(
const nativePluginProxy = (Plugins[pluginName] = new Proxy(
{},
{
get(_, prop) {
Expand All @@ -101,7 +123,10 @@ export const createCapacitor = (win: WindowCapacitor): CapacitorInstance => {
},
},
));
registeredPlugins.set(pluginName, nativePluginProxy);
registeredPlugins.set(pluginName, {
implementations: impls,
proxy: nativePluginProxy,
});
return nativePluginProxy;
}

Expand All @@ -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<any>(
const pluginProxy = (Plugins[pluginName] = new Proxy(
{},
{
get(_, prop) {
Expand Down Expand Up @@ -229,7 +254,10 @@ export const createCapacitor = (win: WindowCapacitor): CapacitorInstance => {
},
},
));
registeredPlugins.set(pluginName, pluginProxy);
registeredPlugins.set(pluginName, {
implementations: impls,
proxy: pluginProxy,
});
return pluginProxy;
};

Expand Down
1 change: 1 addition & 0 deletions core/src/tests/runtime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
36 changes: 36 additions & 0 deletions ios/Capacitor/Capacitor/JSExport.swift
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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!
Expand Down

0 comments on commit 2fbd954

Please sign in to comment.