I\'m trying to use Swift\'s JavaScriptCore framework to take advantage of an existing JavaScript library that uses ES6 modules. Specifically, morse-pro by Stephen C Phillips. I\
I too have same issue in accessing my own JS code, which uses 3 external libraries(Lodash, Moment and Moment-range).
Initially i have tried by importing all 4 .js files(1-My own file and 3-libraries) inside my app bundle. But when im trying like this its not worked. I faced issue in place where i have imported those libraries inside my own JS code.
Workaround:
At last i have copied and pasted all codes from those libraries codes inside my own code and imported only one .js file(my own file) inside my app. So in this case no need to import anything from any library. So my code worked successfully.
NOTE: I am not sure whether my workaround is good approach or not. I just thought to register my issue and workaround which helped me.
My code:
lazy var context: JSContext? = {
let context = JSContext()
guard let timeSlotJS = Bundle.main.path(forResource: "app", ofType: "js") else {
print("Unable to read resource file")
return nil
}
do {
let timeSlotContent = try String(contentsOfFile: timeSlotJS, encoding: String.Encoding.utf8)
_ = context?.evaluateScript(timeSlotContent)
} catch {
print("Error on extracting js content")
}
let _ = context?.evaluateScript("var console = { log: function(message) { _consoleLog(message) } }")
// Print log messages
let consoleLog: @convention(block) (String) -> Void = { message in
print("Javascript log: " + message)
}
context?.setObject(unsafeBitCast(consoleLog, to: AnyObject.self), forKeyedSubscript: "_consoleLog" as NSCopying & NSObjectProtocol)
// Print exception messages
context!.exceptionHandler = { context, exception in
print("JS Error: \(exception!)")
}
return context
}()
func accessingJSCode() {
let timeSlotScript = "DigitalVaultTimeSlotAvailabilityFuntion({\"services\": \(services)}, {\"response\": {\"result\": {\"CustomModule1\": {\"row\":\(rowValue)}}, \"uri\": \"/crm/private/json/CustomModule1/searchRecords\"}});"
print(timeSlotScript)
let timeSlots = context!.evaluateScript(timeSlotScript)
// Then here i parsed my output JSON from script.
}
Thanks!!
While using import
for modules is not possible.
You can support require('path/filename');
syntax.
This is done by providing require as a function to JS. The import
command is (unfortunately) too exotic to implement in the given restrictions of JSContext
.
See the following implementations
@interface MyContext ()
@property JSContext *context;
@implementation MyContext
- (void) setupRequire {
MyContext * __weak weakSelf = self;
self.context[@"require"] = ^(NSString *path) {
path = [weakSelf resolvePath:path];
if(![[NSFileManager defaultManager] fileExistsAtPath:path]) {
NSString *message = [NSString stringWithFormat:@"Require: File “%@” does not exist.", path];
weakSelf.context.exception = [JSValue valueWithNewErrorFromMessage:message inContext:weakSelf.context];
return;
}
[weakSelf loadScript:path];
};
}
- (NSString *) resolvePath:(NSString *)path {
path = path.stringByResolvingSymlinksInPath;
return path.stringByStandardizingPath;
}
- (void) loadScript:(NSString *)path {
NSError *error;
NSString *script = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
if (error) {
script = @"";
NSLog(@"Error: Could not read file in path “%@” to string. (%@)", path, error);
// Return void or throw an error here.
return
}
[self.context evaluateScript:script];
}
This example is based on code in Phoenix https://github.com/kasper/phoenix
https://github.com/kasper/phoenix/blob/master/Phoenix/PHContext.m#L195-L206
This example is based on code in CutBox https://github.com/cutbox/CutBox
import JavascriptCore
class JSService {
let context = JSContext()
let shared = JSService()
let require: @convention(block) (String) -> (JSValue?) = { path in
let expandedPath = NSString(string: path).expandingTildeInPath
// Return void or throw an error here.
guard FileManager.default.fileExists(atPath: expandedPath)
else { debugPrint("Require: filename \(expandedPath) does not exist")
return nil }
guard let fileContent = try? String(contentsOfFile: expandedFilename)
else { return nil }
return JSService.shared.context.evaluateScript(fileContent)
}
init() {
self.context.setObject(self.require,
forKeyedSubscript: "require" as NSString)
}
func repl(_ string: String) -> String {
return self.context.evaluateScript(string).toString()
}
}
After much bumbling around in the dark, I found a way to make the library available to Swift without having to manually alter it.
First, as @estus suggested, I installed the library using NPM, which converts it to ES5 but does not resolve the dependencies. So it's still a bunch of separate files that call each other with require
and export
keywords that neither browsers nor JavaScriptCore understand.
Then I used Browserify to bundle all the dependencies into a single file so that JavaScriptCore could understand it. The normal operation of Browserify hides all the code, so I used the "--standalone" flag to tell it to make marked functions available. If you export the ES5 file directly, it creates a generic object and puts your exported functions under .default
. I want them slightly more accessible, so I created a new file to list the exports and then ran Browserify on that. So for example, a file called "morse-export.js" containing:
module.exports.MorseMessage = require('./lib/morse-pro-message.js').default;
Then I run Browserify on it like this:
browserify ./morse-export.js --standalone Morse > ./morse-bundle.js
And include the morse-bundle.js file in my Swift code using Bundle.main.path(forResource)
. Now I can access the MorseMessage class using Morse.MorseMessage
, so back in Swift:
jsContext?.evaluateScript("var morseMessage = new Morse.MorseMessage()")
print(jsContext!.evaluateScript("morseMessage.translate('abc')"))
prints ".- -... -.-." as you would expect.
The downside of this is that you have to manually add whatever classes and functions you want exported to your export file. Still, this seems to be the simplest way to do it. If there's a better way, I would love to hear about it!