├── .gitignore ├── README.md ├── cli.js ├── extractJS.js ├── index.js ├── package.json └── watch.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.lock 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-swift-bridge 2 | 3 | Utility for never having to write Objective-C bridging code for your swift app again! 4 | 5 | # New In 2.4 6 | 7 | RNSB will now build out the JS definitions of your UI Components automatically! Complete with PropTypes, allowing better autocomplete and checking when building out components on top. 8 | 9 | # Usage 10 | 11 | ``` 12 | cd /path/to/my/module 13 | rnsb 14 | ``` 15 | 16 | This will scan your swift files, make the bridge based on exposed (with the `@objc` attribute) classes, methods and properties. A file called rn-swift-bridge will be created and - if it is not already - added to your module. 17 | 18 | # Watch mode: rnsb --watch 19 | 20 | Watches for changes in swift files in your module, and rebuilds the .m bridge on the fly. 21 | 22 | ``` 23 | cd /path/to/my/module 24 | rnsb --watch 25 | ``` 26 | 27 | # Tips 28 | 29 | 1. The utility does not support `@objcmembers` yet so you need to indicate which classes, methods and properties to expose via individual `@objc` tags 30 | 2. When working with types it cannot identify (e.g. enums that are implicitly ints) it needs a little help on a prior line: add a line comment like so: 31 | 32 | ``` 33 | //@rn type=NSInteger 34 | @objc var worldAlignment:ARConfiguration.WorldAlignment { 35 | ``` 36 | 37 | 3. When working with RCTViewManager, it will auto-detect the name of the view you are managing using the same RN heuristic - the view is the name of the manager minus the word "Manager". So "RHDViewManager" manages "RHDView". However, if your naming conventions do not match to this, you can tell the bridge the name of the view to manage exposing view props. 38 | 39 | ```swift 40 | // @rn view=RHDARView 41 | @objc(RHDARViewManager) 42 | class RHDARViewManager: RCTViewManager { 43 | /*...*/ 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const core = require("./index.js"); 3 | const commander = require("commander"); 4 | const fs = require("fs"); 5 | const watch = require("node-watch"); 6 | const Path = require("path"); 7 | const cp = require("child_process"); 8 | commander.usage("[options] [targetpath]"); 9 | commander.option("-o --out "); 10 | commander.option("-w --watch"); 11 | commander.parse(process.argv); 12 | var thisPath = commander.args[0] ? commander.args[0] : process.cwd(); 13 | var jsFile = commander.js 14 | ? commander.js 15 | : Path.join(thisPath, "RNSwiftBridge.js"); 16 | var outfile = commander.out; 17 | if (!outfile) outfile = core.getRootIOSPath(thisPath); 18 | if (fs.existsSync(outfile) && fs.lstatSync(outfile).isDirectory()) { 19 | outfile = Path.join(outfile, "rn-swift-bridge.m"); 20 | } 21 | if (commander.watch) { 22 | try { 23 | console.log("Watching for swift changes on " + thisPath); 24 | watch(thisPath, { recursive: true, filter: /\.swift$/ }, () => { 25 | const text = core.getBridgingModuleTextFromPath(thisPath); 26 | console.log("Detected change"); 27 | if (core.writeIf(outfile, text)) { 28 | core.addModuleToPBXProj(outfile, thisPath); 29 | console.log("Updated " + outfile); 30 | if (core.writeIf(jsFile, core.getJSFromPath(thisPath))) { 31 | console.log("Updated " + jsFile); 32 | } 33 | } 34 | }); 35 | } catch (e) { 36 | console.log("Hit error ", e); 37 | } 38 | } else { 39 | try { 40 | const text = core.getBridgingModuleTextFromPath(thisPath); 41 | if (core.writeIf(outfile, text)) { 42 | core.addModuleToPBXProj(outfile, thisPath); 43 | } else console.log("No changes to ", outfile); 44 | console.log("Updated " + outfile); 45 | if (core.writeIf(jsFile, core.getJSFromPath(thisPath))) { 46 | console.log("Updated " + jsFile); 47 | } 48 | } catch (e) { 49 | console.log("Hit error ", e); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /extractJS.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const RNSB = require("./index"); 3 | const Path = require("path"); 4 | const fs = require("fs"); 5 | var thisPath = process.cwd(); 6 | if (process.argv[2]) thisPath = process.argv[2]; 7 | const out = RNSB.getJSFromPath(thisPath); 8 | console.log(out); 9 | fs.writeFileSync(Path.join(thisPath, "RNSwiftBridge.js"), out); 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const Path = require("path"); 3 | const glob = require("glob"); 4 | const xcode = require("@raydeck/xcode"); 5 | const prettier = require("prettier"); 6 | function getRootIOSPath(initialPath) { 7 | if (!initialPath) initialPath = process.cwd(); 8 | else initialPath = Path.resolve(process.cwd(), initialPath); 9 | const globs = glob.sync(Path.join(initialPath, "**", "*xcodeproj")); 10 | if (!globs) return false; 11 | return Path.dirname(globs[0]); 12 | } 13 | function getBridgingModuleTextFromPath(initialPath) { 14 | const classInfo = getClassesFromPath(initialPath); 15 | return getBridgingModuleTextFromClasses(classInfo); 16 | } 17 | function getClassesFromPath(initialPath) { 18 | const newdir = getRootIOSPath(initialPath); 19 | const out = processDir(newdir); 20 | if (!out) return false; 21 | //Convert file output to class-based output independent of files 22 | var classes = {}; 23 | Object.keys(out).forEach(path => { 24 | const cls = out[path]; 25 | if (!cls) return; 26 | Object.keys(cls).forEach(cl => { 27 | const obj = cls[cl]; 28 | classes[cl] = obj; 29 | }); 30 | }); 31 | if (!classes) return false; 32 | //Distill processed classes 33 | var processedClasses = {}; 34 | Object.keys(classes).forEach(classname => { 35 | const obj = classes[classname]; 36 | if (!obj.lines) return; 37 | var p = { 38 | name: classname, 39 | subclasses: obj.subclasses 40 | }; 41 | if (obj.view) p.view = obj.view; 42 | if ( 43 | p.subclasses.indexOf("RCTViewManager") > -1 && 44 | !p.view && 45 | classname.endsWith("Manager") 46 | ) { 47 | p.view = classname.substring(0, classname.length - 7); 48 | } 49 | obj.lines.forEach(line => { 50 | switch (line.type) { 51 | case "func": 52 | if (!p.methods) p.methods = {}; 53 | p.methods[line.info.name] = line.info; 54 | break; 55 | case "var": 56 | if (!p.properties) p.properties = {}; 57 | p.properties[line.info.name] = line.info; 58 | break; 59 | case "events": 60 | if (!p.events) p.events = []; 61 | line.info.events.map(e => { 62 | if (p.events.indexOf(e) == -1) p.events.push(e); 63 | }); 64 | break; 65 | case "constants": 66 | if (!p.constants) p.constants = []; 67 | line.info.constants.map(e => { 68 | if (p.constants.indexOf(e) == -1) p.constants.push(e); 69 | }); 70 | } 71 | }); 72 | processedClasses[classname] = p; 73 | }); 74 | return processedClasses; 75 | } 76 | function getBridgingModuleTextFromClasses(processedClasses) { 77 | usedEmitter = false; 78 | usedViewManager = false; 79 | outlines = ["#import "]; 80 | Object.keys(processedClasses).forEach(c => { 81 | //make the class header 82 | //Look for special classes 83 | var obj = processedClasses[c]; 84 | if (!obj.methods && !obj.view) return; 85 | var useEmitter = false; 86 | var useViewManager = false; 87 | if (obj.subclasses) { 88 | if (obj.subclasses.indexOf("RCTEventEmitter") > -1) { 89 | useEmitter = true; 90 | usedEmitter = true; 91 | } 92 | if (obj.subclasses.indexOf("RCTViewManager") > -1) { 93 | useViewManager = true; 94 | usedViewManager = true; 95 | } 96 | } 97 | if (useEmitter) { 98 | outlines.push("@interface RCT_EXTERN_MODULE(" + c + ", RCTEventEmitter)"); 99 | } else if (useViewManager) { 100 | outlines.push("@interface RCT_EXTERN_MODULE(" + c + ", RCTViewManager)"); 101 | } else { 102 | outlines.push("@interface RCT_EXTERN_MODULE(" + c + ", NSObject)"); 103 | } 104 | if (obj.methods) { 105 | Object.keys(obj.methods).forEach(methodName => { 106 | txt = "RCT_EXTERN_METHOD(" + methodName; 107 | const m = obj.methods[methodName]; 108 | if (m.args) { 109 | m.args.forEach(arg => { 110 | if (!arg) return; 111 | var name = arg.name; 112 | var type = arg.type; 113 | var isDefault = arg.isDefault; 114 | if (!isDefault) { 115 | txt += name; 116 | } 117 | txt += ":(" + type + ")" + name + " "; 118 | }); 119 | } 120 | txt = txt.trim() + ");"; 121 | outlines.push(txt); 122 | }); 123 | } 124 | if (useViewManager && obj.view) { 125 | const ps = getProperties(obj.view, processedClasses); 126 | if (ps) { 127 | Object.keys(ps).forEach(propertyName => { 128 | const p = ps[propertyName]; 129 | const type = p.type; 130 | const txt = 131 | "RCT_EXPORT_VIEW_PROPERTY(" + propertyName + ", " + type + ");"; 132 | outlines.push(txt); 133 | }); 134 | } 135 | } 136 | outlines.push("@end"); 137 | }); 138 | 139 | if (usedEmitter) outlines.unshift("#import "); 140 | if (usedViewManager) outlines.unshift("#import "); 141 | const finalText = outlines.join("\n"); 142 | return finalText; 143 | } 144 | function processDir(rootPath) { 145 | var out = {}; 146 | const contents = fs.readdirSync(rootPath).filter(v => { 147 | if (v == "Pods") return false; 148 | if (v.indexOf(".") == 0) return false; 149 | return true; 150 | }); 151 | contents.forEach(subdir => { 152 | const fullSubDir = Path.resolve(rootPath, subdir); 153 | if (fs.lstatSync(fullSubDir).isDirectory()) { 154 | const o = processDir(fullSubDir); 155 | out = { ...out, ...o }; 156 | } else { 157 | const t = processFile(fullSubDir); 158 | if (t) out[fullSubDir] = t; 159 | } 160 | }); 161 | return out; 162 | } 163 | function processFile(filePath) { 164 | const extension = Path.extname(filePath); 165 | if (extension.toLowerCase() !== ".swift") return null; 166 | const txt = fs.readFileSync(filePath, "utf8"); 167 | const lines = txt.split("\n").filter(l => { 168 | if (l.trim().length > 0) return true; 169 | return false; 170 | }); 171 | var foundLines = []; 172 | lines.filter; 173 | for (var x = 0; x < lines.length; x++) { 174 | var line = lines[x]; 175 | if (line.match(/^\s*@objc/)) { 176 | var obj = { line: x, text: line }; 177 | if (x > 0) obj.before = lines[x - 1]; 178 | if (x < lines.length - 1) obj.after = lines[x + 1]; 179 | var l = processLine(obj); 180 | if (l) foundLines.push(l); 181 | } 182 | if (line.match(/@RNS/)) { 183 | var obj = { line: x, text: line }; 184 | if (x > 0) obj.before = lines[x - 1]; 185 | if (x < lines.length - 1) obj.after = lines[x + 1]; 186 | var l = processHint(obj); 187 | if (l) foundLines.push(l); 188 | } 189 | } 190 | //Reprocess lines into classes 191 | var classes = {}; 192 | var thisClass; 193 | foundLines.forEach(obj => { 194 | console.log("This is a class and its object is ", obj); 195 | if (obj.type == "class") { 196 | var name = obj.info.name; 197 | if (obj.objcname) { 198 | name = obj.objcname; 199 | } 200 | thisClass = { name: name, subclasses: obj.info.subclasses, lines: [] }; 201 | if (obj.view) thisClass.view = obj.view; 202 | classes[name] = thisClass; 203 | } else if (thisClass) { 204 | thisClass.lines.push(obj); 205 | } else { 206 | console.log("Hit error situation with line", obj, filePath); 207 | } 208 | }); 209 | return classes; 210 | } 211 | function processHint(v) { 212 | var t = v.text.trim(); 213 | //OK, so what does this tell us? 214 | if (t.indexOf("@RNSEvent") > -1) { 215 | //OK, so there are events on this line. let's take a look 216 | var words = t.split(/[^A-Za-z0-9-_]/).filter(w => { 217 | return ( 218 | w.length > 0 && ["return", "RNSEvent", "RNSEvents"].indexOf(w) == -1 219 | ); 220 | }); 221 | return { type: "events", info: { events: words } }; 222 | } 223 | if (t.indexOf("@RNSConstant") > -1) { 224 | var words = t.split(/[^A-Za-z0-9-_]/).filter(w => { 225 | return ( 226 | w.length > 0 && 227 | ["return", "RNSConstant", "RNSConstants"].indexOf(w) == -1 228 | ); 229 | }); 230 | return { type: "constants", info: { constants: words } }; 231 | } 232 | } 233 | function processLine(v) { 234 | var t = v.text.trim(); 235 | if (v.text.indexOf("@objc") > -1) { 236 | var t = v.text.split("@objc")[1].trim(); 237 | } 238 | var firstspace = t.indexOf(" "); 239 | var type = t.substr(0, firstspace); 240 | if (["public", "private", "open"].indexOf(type) != -1) { 241 | const nextspace = t.indexOf(" ", firstspace + 1); 242 | type = t.substr(firstspace, nextspace - firstspace).trim(); 243 | firstspace = nextspace; 244 | } 245 | if (t.indexOf("class func") > -1) { 246 | //Here's a tricky thing - special exception for class functions, that should never be exported 247 | return null; 248 | } 249 | var rest = t.substr(firstspace); 250 | var info; 251 | [v.before, v.after].forEach(line => { 252 | if (line && line.indexOf("@rn") > -1) { 253 | //after rn look for the tuples 254 | const after = line.substr(line.indexOf("@rn") + 3, line.length); 255 | const tuples = after.split(/[\s&]/); 256 | tuples.forEach(raw => { 257 | if (raw.indexOf("=" > 0)) { 258 | raw = raw.trim(); 259 | const key = raw.substr(0, raw.indexOf("=")).trim(); 260 | const val = raw.substr(raw.indexOf("=") + 1, raw.length).trim(); 261 | if (!key.length) return; 262 | v[key] = val; 263 | } else { 264 | v[raw] = true; 265 | } 266 | }); 267 | } 268 | }); 269 | if (v.before && v.before.indexOf("@rn") > -1) { 270 | } 271 | switch (type) { 272 | case "": 273 | //This could be because I have a class in the next line. Check it out? 274 | if ( 275 | v.after && 276 | (v.after.indexOf("class") > -1 || 277 | v.after.indexOf("func") > -1 || 278 | v.after.indexOf("var") > -1) 279 | ) { 280 | v.objcname = t.substr(1, t.length - 2); 281 | v.text = v.after; 282 | delete v.after; 283 | return processLine(v); 284 | } 285 | return null; 286 | case "class": 287 | //Get the subclasses 288 | //Remove curlybrace 289 | 290 | if (rest.indexOf("{") > -1) { 291 | rest = rest.substr(0, rest.indexOf("{")); 292 | } 293 | if (rest.indexOf(":") > -1) { 294 | var subclasses = rest 295 | .substr(rest.indexOf(":") + 1, rest.length) 296 | .split(",") 297 | .map(v => { 298 | return v.trim(); 299 | }); 300 | info = { 301 | name: rest.substr(0, rest.indexOf(":")).trim(), 302 | subclasses: subclasses 303 | }; 304 | } else { 305 | info = { name: rest.trim() }; 306 | } 307 | if (v.objcname) info.name = v.objcname; 308 | break; 309 | case "func": 310 | const name = rest.substr(0, rest.indexOf("(")).trim(); 311 | var argstr = rest.substr(rest.indexOf("(") + 1, rest.length); 312 | if (argstr.indexOf("{") > -1) { 313 | argstr = argstr.substr(0, argstr.indexOf("{")); 314 | } 315 | if (argstr.indexOf(")") > -1) { 316 | argstr = argstr.substr(0, argstr.indexOf(")")); 317 | } 318 | args = argstr.split(",").map(v => { 319 | return v.trim(); 320 | }); 321 | args = args.map(arg => { 322 | const colonpos = arg.indexOf(":"); 323 | const name = arg.substr(0, colonpos).trim(); 324 | const type = arg.substr(colonpos + 1, arg.length).trim(); 325 | 326 | if (!name) return null; 327 | return { name: name, type: getOCType(type) }; 328 | }); 329 | if (args[0] && args[0].name.indexOf("_") === 0) { 330 | const pieces = args[0].name.split(" "); 331 | args[0].name = pieces[1]; 332 | args[0].isDefault = true; 333 | } 334 | info = { name: name, args: args }; 335 | break; 336 | case "var": 337 | //Check for a type 338 | const colonPos = rest.indexOf(":"); 339 | const eqPos = rest.indexOf(":"); 340 | if (colonPos > -1 && (eqPos > -1 || colonPos > eqPos)) { 341 | const name = rest.substr(0, colonPos).trim(); 342 | if (v.type) { 343 | info = { name: name, type: getOCType(v.type) }; 344 | break; 345 | } 346 | //The word following the colon is the type 347 | var afterColon = rest.substr(colonPos + 1, rest.length); 348 | if (afterColon.indexOf("{") > -1) 349 | afterColon = afterColon.substr(0, afterColon.indexOf("{")); 350 | const eqPos2 = afterColon.indexOf("="); 351 | if (eqPos2 > -1) { 352 | const type = getOCType(afterColon.substr(0, eqPos2).trim()); 353 | info = { name: name, type: type }; 354 | break; 355 | } else { 356 | const type = getOCType(afterColon.trim()); 357 | info = { name: name, type: type }; 358 | break; 359 | } 360 | } 361 | console.log("I don't know what to do with ", rest); 362 | } 363 | return { 364 | type, 365 | info 366 | }; 367 | } 368 | function getOCType(type) { 369 | type = type.trim(); 370 | if (type.substr(-1) == "?") type = type.substr(0, type.length - 1); 371 | if (type.indexOf("@") === 0) 372 | type = type.substr(type.indexOf(" ") + 1, type.length - 1); 373 | switch (type) { 374 | case "Int": 375 | case "Int32": 376 | case "Integer": 377 | return "NSInteger"; 378 | case "Float": 379 | return "float"; 380 | case "Double": 381 | return "double"; 382 | // return "NSNumber *"; 383 | case "NSInteger": 384 | return type; 385 | case "String": 386 | return "NSString *"; 387 | case "jsonType": 388 | return "NSDictionary *"; 389 | case "Bool": 390 | return "BOOL"; 391 | case "URL": 392 | return "NSURL *"; 393 | case "Date": 394 | return "NSDate *"; 395 | case "Any": 396 | return "id"; 397 | default: 398 | //Try some new techniques 399 | if (type.indexOf("[") === 0) { 400 | if (type.indexOf(":") > 0) { 401 | return "NSDictionary *"; 402 | } else { 403 | return "NSArray *"; 404 | } 405 | } 406 | if (type.indexOf("Block") > -1) { 407 | return type; 408 | } else { 409 | return type + " *"; 410 | } 411 | } 412 | return type; 413 | } 414 | function getProperties(className, processedClasses) { 415 | const obj = processedClasses[className]; 416 | if (obj && obj.properties) { 417 | return obj.properties; 418 | } 419 | return null; 420 | } 421 | function writeIf(outfile, text) { 422 | if (fs.existsSync(outfile)) { 423 | const oldText = fs.readFileSync(outfile, "utf8"); 424 | if (oldText == text) { 425 | return false; 426 | } else { 427 | fs.unlinkSync(outfile); 428 | } 429 | } 430 | const result = fs.writeFileSync(outfile, text); 431 | if (result) console.log("Could not write file", outfile); 432 | return true; 433 | } 434 | function getProjectPath(path) { 435 | if (!path) path = process.cwd(); 436 | const iosPath = getRootIOSPath(path); 437 | const globs = glob.sync( 438 | Path.join(iosPath, "**", "*xcodeproj", "project.pbxproj") 439 | ); 440 | if (!globs || !globs.length) return false; 441 | return globs[0]; 442 | } 443 | function addModuleToPBXProj(outfile, iosPath) { 444 | const projpath = getProjectPath(iosPath); 445 | if (!projpath) return false; 446 | const project = xcode.project(projpath); 447 | project.parseSync(); 448 | //Find my file - outfile! 449 | const basename = Path.basename(outfile); 450 | project.addSourceFileNew(basename); 451 | 452 | const out = project.writeSync(); 453 | fs.writeFileSync(projpath, out); 454 | } 455 | function getJSFromPath(thisPath) { 456 | const classes = getClassesFromPath(thisPath); 457 | var methods = 0; 458 | var components = 0; 459 | var exportables = []; 460 | var outlines = []; 461 | var events = []; 462 | var constants = []; 463 | Object.keys(classes).forEach(k => { 464 | const obj = classes[k]; 465 | const NativeObj = "Native" + k; 466 | if (obj.methods) { 467 | outlines.push("//#region Code for object " + k); 468 | outlines.push("const " + NativeObj + " = NativeModules." + k); 469 | Object.keys(obj.methods).forEach(m => { 470 | methods++; 471 | const mobj = obj.methods[m]; 472 | const JSm = exportables.indexOf(m) > -1 ? k + m : m; 473 | const async = 474 | mobj.args.filter(arg => { 475 | return arg && arg.type == "RCTPromiseResolveBlock"; 476 | }).length > 0 477 | ? "async " 478 | : ""; 479 | const isAwait = async ? "await " : ""; 480 | const filteredKeys = mobj.args 481 | .filter(arg => { 482 | return ( 483 | !arg || 484 | ["RCTPromiseRejectBlock", "RCTPromiseResolveBlock"].indexOf( 485 | arg.type 486 | ) == -1 487 | ); 488 | }) 489 | .map(arg => { 490 | return arg ? arg.name : null; 491 | }); 492 | var line = 493 | "const " + 494 | JSm + 495 | " = " + 496 | async + 497 | "(" + 498 | filteredKeys.join(", ") + 499 | ") => {\n return " + 500 | isAwait + 501 | NativeObj + 502 | "." + 503 | m + 504 | "(" + 505 | filteredKeys.join(", ") + 506 | ");\n}"; 507 | outlines.push(line); 508 | exportables.push(JSm); 509 | }); 510 | outlines.push("//#endregion"); 511 | } 512 | if (obj.events) { 513 | outlines.push("//#region events for object " + k); 514 | const nativeEventEmitterFunction = "get" + NativeObj + "EventEmitter"; 515 | outlines.push("var _" + nativeEventEmitterFunction + " = null"); 516 | outlines.push( 517 | "const " + 518 | nativeEventEmitterFunction + 519 | " = () => { if(!_" + 520 | nativeEventEmitterFunction + 521 | ") _" + 522 | nativeEventEmitterFunction + 523 | "= new NativeEventEmitter(" + 524 | NativeObj + 525 | "); return _" + 526 | nativeEventEmitterFunction + 527 | "}" 528 | ); 529 | obj.events.forEach(event => { 530 | const methodName = "subscribeTo" + event; 531 | outlines.push( 532 | "const " + 533 | methodName + 534 | " = cb=>{ return " + 535 | nativeEventEmitterFunction + 536 | '().addListener("' + 537 | event + 538 | '", cb)}' 539 | ); 540 | exportables.push(methodName); 541 | events.push({ event, methodName }); 542 | }); 543 | outlines.push("//#endregion"); 544 | } 545 | if (obj.constants) { 546 | outlines.push("//#region constants for object " + k); 547 | obj.constants.forEach(constant => { 548 | const constantName = constant; 549 | outlines.push( 550 | "const " + constantName + " = " + NativeObj + "." + constant 551 | ); 552 | constants.push({ constant, NativeObj }); 553 | exportables.push(constant); 554 | }); 555 | outlines.push("//#endregion"); 556 | } 557 | if (obj.view) { 558 | components++; 559 | const componentName = obj.view; 560 | const nativeName = "Native" + obj.view; 561 | const requireLine = 562 | "const " + 563 | nativeName + 564 | " = requireNativeComponent('" + 565 | obj.view + 566 | "'," + 567 | componentName + 568 | ")"; 569 | outlines.push(requireLine); 570 | outlines.push("class " + componentName + " extends Component {"); 571 | outlines.push("render() {"); 572 | outlines.push("return <" + nativeName + " {...this.props} />"); 573 | outlines.push("}"); 574 | outlines.push("}"); 575 | outlines.push(componentName + ".propTypes = {"); 576 | if (classes[obj.view] && classes[obj.view].properties) { 577 | Object.keys(classes[obj.view].properties).forEach(propName => { 578 | const pobj = classes[obj.view].properties[propName]; 579 | 580 | outlines.push( 581 | propName + ": " + "PropTypes." + getPropTypeFromObject(pobj) + "," 582 | ); 583 | }); 584 | } 585 | 586 | outlines.push("...ViewPropTypes"); 587 | outlines.push("}"); 588 | exportables.push(componentName); 589 | } 590 | }); 591 | if (events.length > 0) { 592 | outlines.push("//#region Event marshalling object"); 593 | outlines.push("const RNSEvents = {"); 594 | events.forEach(({ event, methodName }) => { 595 | outlines.push(event + ": " + methodName); 596 | outlines.push(","); 597 | }); 598 | outlines.pop(); 599 | outlines.push("}"); 600 | outlines.push("//#endregion"); 601 | exportables.push("RNSEvents"); 602 | } 603 | if (methods > 0) { 604 | outlines.unshift( 605 | 'import { NativeModules, NativeEventEmitter, requireNativeComponent, ViewPropTypes } from "react-native"' 606 | ); 607 | outlines.unshift('import React, { Component } from "react"'); 608 | outlines.unshift('import { PropTypes } from "prop-types"'); 609 | } else if (components > 0) { 610 | outlines.unshift( 611 | 'import { requireNativeComponent, ViewPropTypes } from "react-native"' 612 | ); 613 | outlines.unshift('import React, { Component } from "react"'); 614 | outlines.unshift('import { PropTypes } from "prop-types"'); 615 | } else if (methods > 0) { 616 | outlines.unshift( 617 | 'import { NativeModules, NativeEventEmitter } from "react-native"' 618 | ); 619 | } 620 | outlines.push("//#region Exports"); 621 | outlines.push("export {\n " + exportables.join(",\n ") + "\n}"); 622 | outlines.push("//#endregion"); 623 | // const out = outlines.join("\n"); 624 | const out = prettier.format(outlines.join("\n")); 625 | 626 | return out; 627 | } 628 | function getPropTypeFromObject(pobj) { 629 | switch (pobj.type) { 630 | case "NSString *": 631 | return "string"; 632 | case "BOOL": 633 | return "bool"; 634 | case "NSInteger": 635 | case "float": 636 | case "double": 637 | return "number"; 638 | case "NSArray *": 639 | return "array"; 640 | default: 641 | return "object"; 642 | } 643 | } 644 | module.exports = { 645 | getBridgingModuleTextFromPath, 646 | getBridgingModuleTextFromClasses, 647 | getClassesFromPath, 648 | getRootIOSPath, 649 | writeIf, 650 | addModuleToPBXProj, 651 | getJSFromPath 652 | }; 653 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-swift-bridge", 3 | "version": "2.5.2", 4 | "description": "", 5 | "main": "index.js", 6 | "author": "Ray Deck", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/rhdeck/react-native-swift-bridge.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/rhdeck/react-native-swift-bridge/issues" 14 | }, 15 | "homepage": "https://github.com/rhdeck/react-native-swift-bridge#readme", 16 | "dependencies": { 17 | "commander": "^2.14.0", 18 | "glob": "^7.1.2", 19 | "node-watch": "^0.5.6", 20 | "prettier": "^1.12.1", 21 | "@raydeck/xcode": "^2.2.0" 22 | }, 23 | "bin": { 24 | "react-native-swift-bridge": "cli.js", 25 | "rnsb": "cli.js", 26 | "rnsb-watch": "watch.js", 27 | "rnsb-js": "extractJS.js" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /watch.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const cp = require("child_process"); 3 | const Path = require("path"); 4 | const fs = require("fs"); 5 | const thisPath = fs.realpathSync(process.argv[1]); 6 | const clipath = Path.join(Path.dirname(thisPath), "cli.js"); 7 | const args = [...process.argv.slice(2, process.argv.length), "--watch"]; 8 | cp.spawnSync(clipath, args, { stdio: "inherit" }); 9 | --------------------------------------------------------------------------------