diff --git a/__tests__/async.test.js b/__tests__/async.test.js new file mode 100644 index 0000000..9a8b046 --- /dev/null +++ b/__tests__/async.test.js @@ -0,0 +1,24 @@ +const { readFile, writeFileSync, unlinkSync } = require('../') + +function getScriptFolder(context) { + const parts = context.scriptPath.split('/') + parts.pop() + return parts.join('/') +} + +test('should read a file', (context) => { + const testFilePath = getScriptFolder(context) + '/test.txt' + writeFileSync(testFilePath, 'test') + return new Promise((resolve, reject) => { + readFile(testFilePath, 'utf8', (err, res) => { + if (err) { + reject(err) + return + } + resolve(res) + }) + }).then((res) => { + expect(res).toBe('test') + unlinkSync(testFilePath) + }) +}) diff --git a/cocoascript-class/index.js b/cocoascript-class/index.js new file mode 100644 index 0000000..ee70cff --- /dev/null +++ b/cocoascript-class/index.js @@ -0,0 +1,56 @@ +var runtime = require("./runtime.js"); + +// super when returnType is id and args are void +// id objc_msgSendSuper(struct objc_super *super, SEL op, void) + +var SuperInit = runtime.SuperCall(NSStringFromSelector("init"), [], { type: "@" }); + +var reserved = {'className': 1, 'classname': 1, 'superclass': 1}; + +function getIvar(obj, name) { + const retPtr = MOPointer.new(); + runtime.object_getInstanceVariable(obj, name, retPtr); + return retPtr.value().retain().autorelease(); +} + +// Returns a real ObjC class. No need to use new. +function ObjCClass(defn) { + var superclass = defn.superclass || NSObject; + const className = (defn.className || defn.classname || "ObjCClass") + NSUUID.UUID().UUIDString(); + var cls = MOClassDescription.allocateDescriptionForClassWithName_superclass(className, superclass); + // Add each handler to the class description + var ivars = []; + for (var key in defn) { + var v = defn[key]; + if (typeof v == 'function' && key !== 'init') { + var selector = NSSelectorFromString(key); + cls.addInstanceMethodWithSelector_function(selector, v); + } else if (!reserved[key]) { + ivars.push(key); + cls.addInstanceVariableWithName_typeEncoding(key, "@"); + } + } + + cls.addInstanceMethodWithSelector_function(NSSelectorFromString('init'), function () { + const self = SuperInit.call(this); + ivars.map(function (name) { + Object.defineProperty(self, name, { + get() { + return getIvar(self, name); + }, + set(v) { + runtime.object_setInstanceVariable(self, name, v); + } + }); + self[name] = defn[name]; + }); + // If there is a passsed-in init funciton, call it now. + if (typeof defn.init == 'function') defn.init.call(this); + return self; + }); + + return cls.registerClass(); +} + +module.exports = ObjCClass; +module.exports.SuperCall = runtime.SuperCall; diff --git a/cocoascript-class/runtime.js b/cocoascript-class/runtime.js new file mode 100644 index 0000000..9815d32 --- /dev/null +++ b/cocoascript-class/runtime.js @@ -0,0 +1,102 @@ +const objc_super_typeEncoding = '{objc_super="receiver"@"super_class"#}'; + +// You can store this to call your function. this must be bound to the current instance. +function SuperCall(selector, argTypes, returnType) { + const func = CFunc("objc_msgSendSuper", [{ type: '^' + objc_super_typeEncoding }, { type: ":" }, ...argTypes], returnType); + return function (...args) { + const struct = make_objc_super(this, this.superclass()); + const structPtr = MOPointer.alloc().initWithValue_(struct); + return func(structPtr, selector, ...args); + }; +} + +// Recursively create a MOStruct +function makeStruct(def) { + if (typeof def !== 'object' || Object.keys(def).length == 0) { + return def; + } + const name = Object.keys(def)[0]; + const values = def[name]; + + const structure = MOStruct.structureWithName_memberNames_runtime(name, Object.keys(values), Mocha.sharedRuntime()); + + Object.keys(values).map(member => { + structure[member] = makeStruct(values[member]); + }); + + return structure; +} + +function make_objc_super(self, cls) { + return makeStruct({ + objc_super: { + receiver: self, + super_class: cls + } + }); +} + +// Due to particularities of the JS bridge, we can't call into MOBridgeSupport objects directly +// But, we can ask key value coding to do the dirty work for us ;) +function setKeys(o, d) { + const funcDict = NSMutableDictionary.dictionary(); + funcDict.o = o; + Object.keys(d).map(k => funcDict.setValue_forKeyPath(d[k], "o." + k)); +} + +// Use any C function, not just ones with BridgeSupport +function CFunc(name, args, retVal) { + function makeArgument(a) { + if (!a) return null; + const arg = MOBridgeSupportArgument.alloc().init(); + setKeys(arg, { + type64: a.type + }); + return arg; + } + const func = MOBridgeSupportFunction.alloc().init(); + setKeys(func, { + name: name, + arguments: args.map(makeArgument), + returnValue: makeArgument(retVal) + }); + return func; +} + +/* +@encode(char*) = "*" +@encode(id) = "@" +@encode(Class) = "#" +@encode(void*) = "^v" +@encode(CGRect) = "{CGRect={CGPoint=dd}{CGSize=dd}}" +@encode(SEL) = ":" +*/ + +function addStructToBridgeSupport(key, structDef) { + // OK, so this is probably the nastiest hack in this file. + // We go modify MOBridgeSupportController behind its back and use kvc to add our own definition + // There isn't another API for this though. So the only other way would be to make a real bridgesupport file. + const symbols = MOBridgeSupportController.sharedController().valueForKey('symbols'); + if (!symbols) throw Error("Something has changed within bridge support so we can't add our definitions"); + // If someone already added this definition, don't re-register it. + if (symbols[key] !== null) return; + const def = MOBridgeSupportStruct.alloc().init(); + setKeys(def, { + name: key, + type: structDef.type + }); + symbols[key] = def; +}; + +// This assumes the ivar is an object type. Return value is pretty useless. +const object_getInstanceVariable = CFunc("object_getInstanceVariable", [{ type: "@" }, { type: '*' }, { type: "^@" }], { type: "^{objc_ivar=}" }); +// Again, ivar is of object type +const object_setInstanceVariable = CFunc("object_setInstanceVariable", [{ type: "@" }, { type: '*' }, { type: "@" }], { type: "^{objc_ivar=}" }); + +// We need Mocha to understand what an objc_super is so we can use it as a function argument +addStructToBridgeSupport('objc_super', { type: objc_super_typeEncoding }); + +module.exports.SuperCall = SuperCall; +module.exports.CFunc = CFunc; +module.exports.object_getInstanceVariable = object_getInstanceVariable; +module.exports.object_setInstanceVariable = object_setInstanceVariable; diff --git a/index.js b/index.js index 79bbc6d..7a557ef 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,11 @@ // TODO: async. Should probably be done with NSFileHandle and some notifications // TODO: file descriptor. Needs to be done with NSFileHandle var Buffer = require('buffer').Buffer +var createFiber = require('sketch/async').createFiber +var ObjCClass = require('./cocoascript-class') + +// We create one ObjC class for ourselves here so we don't recreate it every time +var ReadFileDelegateClass function encodingFromOptions(options, defaultValue) { return options && options.encoding @@ -145,6 +150,75 @@ module.exports.readdirSync = function(path) { return arr } +module.exports.readFile = function(path, options, callback) { + var encoding = encodingFromOptions(options, 'buffer') + var fileInstance = NSFileHandle.fileHandleForReadingAtPath(path) + if (!fileInstance) { + callback( + new Error("ENOENT Couldn't read the file.") + ) + return + } + + var notificationCenter = NSNotificationCenter.defaultCenter() + + if (!ReadFileDelegateClass) { + ReadFileDelegateClass = ObjCClass({ + classname: 'ReadFileDelegateClass', + utils: null, + + 'onReadCompleted:': function(notification) { + if (notification.userInfo().NSFileHandleError) { + this.utils.callback( + new Error("Couldn't read the file. Error code: " + notification.userInfo().NSFileHandleError) + ) + } else { + // we need to cast to data because buffer doesn't realize it's an NSData + var data = NSString.alloc() + .initWithData_encoding(notification.userInfo().NSFileHandleNotificationDataItem, NSISOLatin1StringEncoding) + .dataUsingEncoding(NSISOLatin1StringEncoding) + var buffer = Buffer.from(data) + if (this.utils.encoding === 'buffer') { + this.utils.callback(null, buffer) + } else if (this.utils.encoding === 'NSData') { + this.utils.callback(null, buffer.toNSData()) + } else { + this.utils.callback(null, buffer.toString(this.utils.encoding)) + } + } + + this.utils.fiber.cleanup() + } + }) + } + + var fiber = createFiber() + + var observerInstance = ReadFileDelegateClass.new() + observerInstance.utils = NSDictionary.dictionaryWithDictionary({ + encoding: encoding, + fiber: fiber, + callback: callback, + }) + + fiber.onCleanup(function() { + // need to unregister the observer when the fiber is cleaned up + notificationCenter.removeObserver_name_object( + observerInstance, + NSFileHandleReadToEndOfFileCompletionNotification, + fileInstance + ) + }) + + notificationCenter.addObserver_selector_name_object( + observerInstance, + NSSelectorFromString('onReadCompleted:'), + NSFileHandleReadToEndOfFileCompletionNotification, + fileInstance + ) + fileInstance.readToEndOfFileInBackgroundAndNotify() +} + module.exports.readFileSync = function(path, options) { var encoding = encodingFromOptions(options, 'buffer') var fileManager = NSFileManager.defaultManager() diff --git a/webpack.skpm.config.js b/webpack.skpm.config.js new file mode 100644 index 0000000..b95a31b --- /dev/null +++ b/webpack.skpm.config.js @@ -0,0 +1,8 @@ +const path = require('path') + +module.exports = (existingConfig, isCommand) => { + if (!isCommand) { + return + } + existingConfig.module.rules[0].include = [path.resolve(__dirname, "__tests__")] +}