Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

readFile async API #12

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions __tests__/async.test.js
Original file line number Diff line number Diff line change
@@ -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)
})
})
56 changes: 56 additions & 0 deletions cocoascript-class/index.js
Original file line number Diff line number Diff line change
@@ -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;
102 changes: 102 additions & 0 deletions cocoascript-class/runtime.js
Original file line number Diff line number Diff line change
@@ -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;
74 changes: 74 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand Down
8 changes: 8 additions & 0 deletions webpack.skpm.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const path = require('path')

module.exports = (existingConfig, isCommand) => {
if (!isCommand) {
return
}
existingConfig.module.rules[0].include = [path.resolve(__dirname, "__tests__")]
}