Skip to content

Commit

Permalink
fix: generate Capacitor.Plugins object (#4496)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcesarmobile committed Apr 28, 2021
1 parent 60e23c2 commit 1c71b7a
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 5 deletions.
87 changes: 86 additions & 1 deletion android/capacitor/src/main/java/com/getcapacitor/JSExport.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
import static com.getcapacitor.FileUtils.readFile;

import android.content.Context;
import android.text.TextUtils;
import java.io.IOException;
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 {

private static String CATCHALL_OPTIONS_PARAM = "_options";
private static String CALLBACK_PARAM = "_callback";

public static String getGlobalJS(Context context, boolean isDebug) {
return "window.Capacitor = { DEBUG: " + isDebug + ", Plugins: {} };";
}
Expand All @@ -36,13 +42,38 @@ 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");
for (PluginHandle plugin : plugins) {
lines.add(
"(function(w) {\n" +
"var a = (w.Capacitor = w.Capacitor || {});\n" +
"var p = (a.Plugins = a.Plugins || {});\n" +
"var t = (p['" +
plugin.getId() +
"'] = {});\n" +
"t.addListener = function(eventName, callback) {\n" +
" return w.Capacitor.addListener('" +
plugin.getId() +
"', 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 "window.Capacitor.PluginHeaders = " + pluginArray.toString() + ";";
return TextUtils.join("\n", lines) + "\nwindow.Capacitor.PluginHeaders = " + pluginArray.toString() + ";";
}

public static String getCordovaPluginJS(Context context) {
Expand Down Expand Up @@ -103,4 +134,58 @@ private static JSONObject createPluginMethodHeader(PluginMethodHandle method) {
public static String getBridgeJS(Context context) throws JSExportException {
return getFilesContent(context, "native-bridge.js");
}

private static String generateMethodJS(PluginHandle plugin, PluginMethodHandle method) {
List<String> lines = new ArrayList<>();

List<String> args = new ArrayList<>();
// Add the catch all param that will take a full javascript object to pass to the plugin
args.add(CATCHALL_OPTIONS_PARAM);

String returnType = method.getReturnType();
if (returnType.equals(PluginMethod.RETURN_CALLBACK)) {
args.add(CALLBACK_PARAM);
}

// Create the method function declaration
lines.add("t['" + method.getName() + "'] = function(" + TextUtils.join(", ", args) + ") {");

switch (returnType) {
case PluginMethod.RETURN_NONE:
lines.add(
"return w.Capacitor.nativeCallback('" +
plugin.getId() +
"', '" +
method.getName() +
"', " +
CATCHALL_OPTIONS_PARAM +
")"
);
break;
case PluginMethod.RETURN_PROMISE:
lines.add(
"return w.Capacitor.nativePromise('" + plugin.getId() + "', '" + method.getName() + "', " + CATCHALL_OPTIONS_PARAM + ")"
);
break;
case PluginMethod.RETURN_CALLBACK:
lines.add(
"return w.Capacitor.nativeCallback('" +
plugin.getId() +
"', '" +
method.getName() +
"', " +
CATCHALL_OPTIONS_PARAM +
", " +
CALLBACK_PARAM +
")"
);
break;
default:
// TODO: Do something here?
}

lines.add("}");

return TextUtils.join("\n", lines);
}
}
81 changes: 77 additions & 4 deletions ios/Capacitor/Capacitor/JSExport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,39 @@ internal class JSExport {
Export the JS required to implement the given plugin.
*/
static func exportJS(userContentController: WKUserContentController, pluginClassName: String, pluginType: CAPPlugin.Type) {
var lines = [String]()

lines.append("""
(function(w) {
var a = (w.Capacitor = w.Capacitor || {});
var p = (a.Plugins = a.Plugins || {});
var t = (p['\(pluginClassName)'] = {});
t.addListener = function(eventName, callback) {
return w.Capacitor.addListener('\(pluginClassName)', eventName, callback);
}
""")
if let bridgeType = pluginType as? CAPBridgedPlugin.Type, let methods = bridgeType.pluginMethods() as? [CAPPluginMethod] {
for method in methods {
lines.append(generateMethod(pluginClassName: pluginClassName, method: method))
}
}

lines.append("""
})(window);
""")
if let data = try? JSONEncoder().encode(createPluginHeader(pluginClassName: pluginClassName, pluginType: pluginType)),
let header = String(data: data, encoding: .utf8) {
let script = """
lines.append("""
(function(w) {
var a = (w.Capacitor = w.Capacitor || {});
var h = (a.PluginHeaders = a.PluginHeaders || []);
h.push(\(header));
})(window);
"""
let userScript = WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContentController.addUserScript(userScript)
""")
}
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? {
Expand All @@ -104,6 +125,58 @@ internal class JSExport {
return PluginHeaderMethod(name: method.name, rtype: rtype)
}

private static func generateMethod(pluginClassName: String, method: CAPPluginMethod) -> String {
let methodName = method.name!
let returnType = method.returnType!
var paramList = [String]()

// add the catch-all
// options argument which takes a full object and converts each
// key/value pair into an option for plugin call.
paramList.append(catchallOptionsParameter)

// Automatically add the _callback param if returning data through a callback
if returnType == CAPPluginReturnCallback {
paramList.append(callbackParameter)
}

// Create a param string of the form "param1, param2, param3"
let paramString = paramList.joined(separator: ", ")

// Generate the argument object that will be sent on each call
let argObjectString = catchallOptionsParameter

var lines = [String]()

// Create the function declaration
lines.append("t['\(method.name!)'] = function(\(paramString)) {")

// Create the call to Capacitor ...
if returnType == CAPPluginReturnNone {
// ...using none
lines.append("""
return w.Capacitor.nativeCallback('\(pluginClassName)', '\(methodName)', \(argObjectString));
""")
} else if returnType == CAPPluginReturnPromise {

// ...using a promise
lines.append("""
return w.Capacitor.nativePromise('\(pluginClassName)', '\(methodName)', \(argObjectString));
""")
} else if returnType == CAPPluginReturnCallback {
// ...using a callback
lines.append("""
return w.Capacitor.nativeCallback('\(pluginClassName)', '\(methodName)', \(argObjectString), \(callbackParameter));
""")
} else {
CAPLog.print("Error: plugin method return type \(returnType) is not supported!")
}

// Close the function
lines.append("}")
return lines.joined(separator: "\n")
}

static func exportCordovaPluginsJS(userContentController: WKUserContentController) throws {
if let pluginsJSFolder = Bundle.main.url(forResource: "public/plugins", withExtension: nil) {
self.injectFilesForFolder(folder: pluginsJSFolder, userContentController: userContentController)
Expand Down

0 comments on commit 1c71b7a

Please sign in to comment.