Merge pull request #13 from dmotz/cli-launch

0.1.0
This commit is contained in:
Dan Motzenbecker 2015-10-17 13:30:42 -04:00
commit 498de825d2
8 changed files with 486 additions and 220 deletions

View File

@ -11,7 +11,7 @@ setting up a React Native app running on ClojureScript.
It stands firmly on the shoulders of giants, specifically those of
[Mike Fikes](http://blog.fikesfarm.com) who created
[Ambly](https://github.com/omcljs/ambly) and the
[documentation](https://github.com/omcljs/ambly/wiki/ClojureScript-React-Native-Quick-Start)
[documentation](http://cljsrn.org/ambly.html)
on setting up a ClojureScript React Native app.
@ -26,31 +26,34 @@ Then, install the CLI using npm:
$ npm install -g natal
```
Then run `natal` with your app's name as the first argument:
To bootstrap a new app, run `natal init` with your app's name as an argument:
```
$ natal FutureApp
$ natal init FutureApp
```
If your app is more than a single word, be sure to type it in CamelCase.
If your app's name is more than a single word, be sure to type it in CamelCase.
A corresponding hyphenated Clojure namespace will be created.
When Xcode appears, click the play button (or ⌘-R) to run the app on the simulator.
If all goes well your app should compile and boot in the simulator.
Then run the following for an interactive workflow:
From there you can begin an interactive workflow by starting the REPL.
```
$ cd future-app
$ ./start.sh
$ natal repl
```
First, choose the correct device (probably `[1]`). At the REPL prompt type this:
Choose your app from the list the REPL outputs (probably `1`) so Ambly can connect.
At the prompt, try loading your app's namespace:
```clojure
(in-ns 'future-app.core)
```
Changes you make via the REPL or by changing your .cljs files should appear live.
Changes you make via the REPL or by changing your `.cljs` files should appear live
in the simulator.
Try this command as an example:
@ -58,8 +61,8 @@ Try this command as an example:
(swap! app-state assoc :text "Hello Native World")
```
When the REPL starts it will print the location of its compilation log.
It's useful to tail it to see any errors, like so:
When the REPL connects to the simulator it will print the location of its
compilation log. It's useful to tail it to see any errors, like so:
```
$ tail -f /Volumes/Ambly-81C53995/watch.log
@ -67,11 +70,27 @@ $ tail -f /Volumes/Ambly-81C53995/watch.log
## Tips
- Having `rlwrap` installed is optional but recommended since it makes the REPL
a much nicer experience with arrow keys
- Having `rlwrap` installed is optional but highly recommended since it makes
the REPL a much nicer experience with arrow keys.
- Don't press ⌘-R in the simulator; code changes should be reflected automatically.
See [this issue](https://github.com/omcljs/ambly/issues/97) in Ambly for details
- Running multiple React Native apps at once can cause problems
See [this issue](https://github.com/omcljs/ambly/issues/97) in Ambly for details.
- Running multiple React Native apps at once can cause problems with the React
Packager so try to avoid doing so.
- You can launch your app on the simulator without opening Xcode by running
`natal launch` in your app's root directory.
- By default new Natal projects will launch on the iPhone 6 simulator. To change
which device `natal launch` uses, you can run `natal listdevices` to see a list
of available simulators, then select one by running `natal setdevice` with the
index of the device on the list.
- To change advanced settings run `natal xcode` to quickly open the Xcode project.
- The Xcode-free workflow is for convenience. If you're encountering app crashes,
you should open the Xcode project and run it from there to view errors.
## Dependencies
@ -86,17 +105,21 @@ tools.
- [Java 8](http://www.oracle.com/technetwork/java/javase/downloads/index.html)
- [CocoaPods](https://cocoapods.org) `>=0.38.2`
- [Ruby](https://www.ruby-lang.org) `>=2.0.0`
- [Xcode](https://developer.apple.com/xcode) `>=6.3`
- [Xcode](https://developer.apple.com/xcode) (+ Command Line Tools) `>=6.3`
- [OS X](http://www.apple.com/osx) `>=10.10`
- [Watchman](https://facebook.github.io/watchman) `>=3.7.0`
## Aspirations
- [x] Xcode-free workflow with CLI tools
- [ ] Automatic wrapping of all React Native component functions for ClojureScript
- [ ] Xcode-free development with CLI tools
- [ ] Automatically run React packager in background
- [ ] Automatically tail cljs build log and report compile errors
- [ ] Templates for other ClojureScript React wrappers
- [ ] Automatic bundling for offline device usage and App Store distribution
- [ ] Android support
Contributions are welcome.
For more ClojureScript React Native resources visit [cljsrn.org](http://cljsrn.org).

View File

@ -1,4 +1,4 @@
#!/usr/bin/env node
require('coffee-script/register');
require('./main');
require('./natal');

View File

@ -1,176 +0,0 @@
# Natal
# Bootstrap ClojureScript React Native apps
# Dan Motzenbecker
# http://oxism.com
# MIT License
fs = require 'fs'
crypto = require 'crypto'
{execSync} = require 'child_process'
chalk = require 'chalk'
semver = require 'semver'
reactInit = require 'react-native/local-cli/init'
rnVersion = require(__dirname + '/package.json').dependencies['react-native']
resources = __dirname + '/resources/'
camelRx = /([a-z])([A-Z])/g
projNameRx = /\$PROJECT_NAME\$/g
projNameHyphRx = /\$PROJECT_NAME_HYPHENATED\$/g
projNameUnderRx = /\$PROJECT_NAME_UNDERSCORED\$/g
log = (s, color = 'green') ->
console.log chalk[color] s
logErr = (err, color = 'red') ->
console.error chalk[color] err
editSync = (path, pairs) ->
fs.writeFileSync path, pairs.reduce (contents, [rx, replacement]) ->
contents.replace rx, replacement
, fs.readFileSync path, encoding: 'ascii'
init = (projName) ->
projNameHyph = projName.replace(camelRx, '$1-$2').toLowerCase()
projNameUs = projName.replace(camelRx, '$1_$2').toLowerCase()
try
log "Creating #{ projName }", 'bgMagenta'
log ''
if fs.existsSync projNameHyph
throw new Error "Directory #{ projNameHyph } already exists"
execSync 'type lein'
execSync 'type pod'
podVersion = execSync('pod --version').toString().trim()
unless semver.satisfies podVersion, '>=0.36.4'
throw new Error "Natal requires CocoaPods 0.36.4 or higher (you have #{ podVersion }).
\nRun [sudo] gem update cocoapods and try again."
log 'Creating Leiningen project'
execSync "lein new #{ projNameHyph }"
log 'Updating Leiningen project'
process.chdir projNameHyph
execSync "cp #{ resources }project.clj project.clj"
editSync 'project.clj', [[projNameHyphRx, projNameHyph]]
corePath = "src/#{ projNameUs }/core.clj"
fs.unlinkSync corePath
corePath += 's'
execSync "cp #{ resources }core.cljs #{ corePath }"
editSync corePath, [[projNameHyphRx, projNameHyph], [projNameRx, projName]]
execSync "cp #{ resources }ambly.sh start.sh"
editSync 'start.sh', [[projNameUnderRx, projNameUs]]
log 'Compiling ClojureScript'
execSync 'lein cljsbuild once dev'
log 'Creating React Native skeleton'
fs.mkdirSync 'iOS'
process.chdir 'iOS'
_log = console.log
global.console.log = ->
reactInit '.', projName
global.console.log = _log
fs.writeFileSync 'package.json', JSON.stringify
name: projName
version: '0.0.1'
private: true
scripts:
start: 'node_modules/react-native/packager/packager.sh'
dependencies:
'react-native': rnVersion
, null, 2
execSync 'npm i', stdio: 'ignore'
log 'Installing Pod dependencies'
process.chdir 'iOS'
execSync "cp #{ resources }Podfile ."
execSync 'pod install', stdio: 'ignore'
log 'Updating Xcode project'
for ext in ['m', 'h']
path = "#{ projName }/AppDelegate.#{ ext }"
execSync "cp #{ resources }AppDelegate.#{ ext } #{ path }"
editSync path, [[projNameRx, projName], [projNameHyphRx, projNameHyph]]
uuid1 = crypto
.createHash 'md5'
.update projName, 'utf8'
.digest('hex')[...24]
.toUpperCase()
uuid2 = uuid1.split ''
uuid2.splice 7, 1, ((parseInt(uuid1[7], 16) + 1) % 16).toString(16).toUpperCase()
uuid2 = uuid2.join ''
editSync \
"#{ projName }.xcodeproj/project.pbxproj",
[
[
/OTHER_LDFLAGS = "-ObjC";/g
'OTHER_LDFLAGS = "${inherited}";'
]
[
/\/\* End PBXBuildFile section \*\//
"\t\t#{ uuid2 } /* out in Resources */ =
{isa = PBXBuildFile; fileRef = #{ uuid1 } /* out */; };
\n/* End PBXBuildFile section */"
]
[
/\/\* End PBXFileReference section \*\//
"\t\t#{ uuid1 } /* out */ = {isa = PBXFileReference; lastKnownFileType
= folder; name = out; path = ../../../target/out;
sourceTree = \"<group>\"; };\n/* End PBXFileReference section */"
]
[
/main.jsbundle \*\/\,/
"main.jsbundle */,\n\t\t\t\t#{ uuid1 } /* out */,"
]
[
/\/\* LaunchScreen.xib in Resources \*\/\,/
"/* LaunchScreen.xib in Resources */,
\n\t\t\t\t#{ uuid2 } /* out in Resources */,"
]
]
execSync "open #{ projName }.xcworkspace"
log '\nWhen Xcode appears, click the play button to run the app on the simulator.', 'yellow'
log 'Then run the following for an interactive workflow:', 'yellow'
log "cd #{ projNameHyph }", 'inverse'
log './start.sh', 'inverse'
log 'First, choose the correct device (Probably [1]).', 'yellow'
log 'At the REPL prompt type this:', 'yellow'
log "(in-ns '#{ projNameHyph }.core)", 'inverse'
log 'Changes you make via the REPL or by changing your .cljs files should appear live.', 'yellow'
log 'Try this command as an example:', 'yellow'
log '(swap! app-state assoc :text "Hello Native World")', 'inverse'
log ''
log '✔ Done', 'bgMagenta'
log ''
catch e
if e.message.match /type\:.+lein/i
logErr 'Leiningen is required (http://leiningen.org/)'
else if e.message.match /type\:.+pod/i
logErr 'CocoaPods is required (https://cocoapods.org/)'
else
logErr e.message
process.exit 1
[_, _, name] = process.argv
unless name
logErr 'You must pass a project name as the first argument.'
logErr 'e.g. natal HelloWorld'
process.exit 1
init name

438
natal.coffee Normal file
View File

@ -0,0 +1,438 @@
# Natal
# Bootstrap ClojureScript React Native apps
# Dan Motzenbecker
# http://oxism.com
# MIT License
fs = require 'fs'
crypto = require 'crypto'
child = require 'child_process'
cli = require 'commander'
chalk = require 'chalk'
semver = require 'semver'
pkgJson = require __dirname + '/package.json'
nodeVersion = pkgJson.engines.node
resources = __dirname + '/resources/'
camelRx = /([a-z])([A-Z])/g
projNameRx = /\$PROJECT_NAME\$/g
projNameHyphRx = /\$PROJECT_NAME_HYPHENATED\$/g
projNameUnderRx = /\$PROJECT_NAME_UNDERSCORED\$/g
rnVersion = '0.13.0-rc'
podMinVersion = '0.38.2'
process.title = 'natal'
log = (s, color = 'green') ->
console.log chalk[color] s
logErr = (err, color = 'red') ->
console.error chalk[color] err
process.exit 1
exec = (cmd, keepOutput) ->
if keepOutput
child.execSync cmd
else
child.execSync cmd, stdio: 'ignore'
readFile = (path) ->
fs.readFileSync path, encoding: 'ascii'
edit = (path, pairs) ->
fs.writeFileSync path, pairs.reduce (contents, [rx, replacement]) ->
contents.replace rx, replacement
, readFile path
pluckUuid = (line) ->
line.match(/\[(.+)\]/)[1]
toUnderscored = (s) ->
s.replace(camelRx, '$1_$2').toLowerCase()
writeConfig = (config) ->
try
fs.writeFileSync '.natal', JSON.stringify config, null, 2
catch {message}
logErr \
if message.match /EACCES/i
'Invalid write permissions for creating .natal config file'
else
message
readConfig = ->
try
JSON.parse readFile '.natal'
catch {message}
logErr \
if message.match /ENOENT/i
'No Natal config was found in this directory (.natal)'
else if message.match /EACCES/i
'No read permissions for .natal'
else if message.match /Unexpected/i
'.natal contains malformed JSON'
else
message
getBundleId = (name) ->
try
if line = readFile "native/ios/#{name}.xcodeproj/project.pbxproj"
.match /PRODUCT_BUNDLE_IDENTIFIER = (.+);/
line[1]
else if line = readFile "native/ios/#{name}/Info.plist"
.match /\<key\>CFBundleIdentifier\<\/key\>\n?\s*\<string\>(.+)\<\/string\>/
rfcIdRx = /\$\(PRODUCT_NAME\:rfc1034identifier\)/
if line[1].match rfcIdRx
line[1].replace rfcIdRx, name
else
line[1]
else
throw new Error 'Cannot find bundle identifier in project.pbxproj or Info.plist'
catch {message}
logErr message
init = (projName) ->
projNameHyph = projName.replace(camelRx, '$1-$2').toLowerCase()
projNameUs = toUnderscored projName
try
log "Creating #{projName}", 'bgMagenta'
log ''
if fs.existsSync projNameHyph
throw new Error "Directory #{projNameHyph} already exists"
exec 'type lein'
exec 'type pod'
exec 'type watchman'
exec 'type xcodebuild'
podVersion = exec('pod --version', true).toString().trim()
unless semver.satisfies podVersion, ">=#{podMinVersion}"
throw new Error """
Natal requires CocoaPods #{podMinVersion} or higher (you have #{podVersion}).
Run [sudo] gem update cocoapods and try again.
"""
log 'Creating Leiningen project'
exec "lein new #{projNameHyph}"
log 'Updating Leiningen project'
process.chdir projNameHyph
exec "cp #{resources}project.clj project.clj"
edit 'project.clj', [[projNameHyphRx, projNameHyph]]
corePath = "src/#{projNameUs}/core.clj"
fs.unlinkSync corePath
corePath += 's'
exec "cp #{resources}core.cljs #{corePath}"
edit corePath, [[projNameHyphRx, projNameHyph], [projNameRx, projName]]
log 'Compiling ClojureScript'
exec 'lein cljsbuild once dev'
log 'Creating React Native skeleton'
fs.mkdirSync 'native'
process.chdir 'native'
fs.writeFileSync 'package.json', JSON.stringify
name: projName
version: '0.0.1'
private: true
scripts:
start: 'node_modules/react-native/packager/packager.sh'
dependencies:
'react-native': rnVersion
, null, 2
exec 'npm i'
exec "
node -e
\"process.argv[3]='#{projName}';
require('react-native/local-cli/init')('.', '#{projName}')\"
"
exec 'rm -rf android'
fs.unlinkSync 'index.android.js'
log 'Installing Pod dependencies'
process.chdir 'ios'
exec "cp #{resources}Podfile ."
exec 'pod install'
log 'Updating Xcode project'
for ext in ['m', 'h']
path = "#{projName}/AppDelegate.#{ext}"
exec "cp #{resources}AppDelegate.#{ext} #{path}"
edit path, [[projNameRx, projName], [projNameHyphRx, projNameHyph]]
uuid1 = crypto
.createHash 'md5'
.update projName, 'utf8'
.digest('hex')[...24]
.toUpperCase()
uuid2 = uuid1.split ''
uuid2.splice 7, 1, ((parseInt(uuid1[7], 16) + 1) % 16).toString(16).toUpperCase()
uuid2 = uuid2.join ''
edit \
"#{projName}.xcodeproj/project.pbxproj",
[
[
/OTHER_LDFLAGS = "-ObjC";/g
'OTHER_LDFLAGS = "${inherited}";'
]
[
/\/\* End PBXBuildFile section \*\//
"\t\t#{uuid2} /* out in Resources */ =
{isa = PBXBuildFile; fileRef = #{uuid1} /* out */; };
\n/* End PBXBuildFile section */"
]
[
/\/\* End PBXFileReference section \*\//
"\t\t#{uuid1} /* out */ = {isa = PBXFileReference; lastKnownFileType
= folder; name = out; path = ../../target/out;
sourceTree = \"<group>\"; };\n/* End PBXFileReference section */"
]
[
/main.jsbundle \*\/\,/
"main.jsbundle */,\n\t\t\t\t#{uuid1} /* out */,"
]
[
/\/\* LaunchScreen.xib in Resources \*\/\,/
"/* LaunchScreen.xib in Resources */,
\n\t\t\t\t#{uuid2} /* out in Resources */,"
]
]
testId = readFile("#{projName}.xcodeproj/project.pbxproj")
.match(new RegExp "([0-9A-F]+) \/\\* #{projName}Tests \\*\/ = \\{")[1]
edit \
"#{projName}.xcodeproj/xcshareddata/xcschemes/#{projName}.xcscheme",
[
[
/\<Testables\>\n\s*\<\/Testables\>/
"""
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "#{testId}"
BuildableName = "#{projName}Tests.xctest"
BlueprintName = "#{projName}Tests"
ReferencedContainer = "container:#{projName}.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
"""
]
]
log 'Creating Natal config'
process.chdir '../..'
config =
name: projName
device: pluckUuid getDeviceList().find (line) -> /iPhone 6/.test line
writeConfig config
launch config
log ''
log 'To get started with your new app, first cd into its directory:', 'yellow'
log "cd #{projNameHyph}", 'inverse'
log ''
log 'Boot the REPL by typing:', 'yellow'
log 'natal repl', 'inverse'
log 'Then choose the correct device to connect to (probably 1).', 'yellow'
log ''
log 'At the REPL prompt type this:', 'yellow'
log "(in-ns '#{projNameHyph}.core)", 'inverse'
log ''
log 'Changes you make via the REPL or by changing your .cljs files should appear live.', 'yellow'
log ''
log 'Try this command as an example:', 'yellow'
log '(swap! app-state assoc :text "Hello Native World")', 'inverse'
log ''
log '✔ Done', 'bgMagenta'
log ''
catch {message}
logErr \
if message.match /type\:.+lein/i
'Leiningen is required (http://leiningen.org)'
else if message.match /type\:.+pod/i
'CocoaPods is required (https://cocoapods.org)'
else if message.match /type\:.+watchman/i
'Watchman is required (https://facebook.github.io/watchman)'
else if message.match /type\:.+xcodebuild/i
'Xcode Command Line Tools are required'
else
message
launch = ({name, device}) ->
log 'Compiling Xcode project'
try
exec "
xcodebuild
-workspace native/ios/#{name}.xcworkspace
-scheme #{name}
-destination platform='iOS Simulator',OS=latest,id='#{device}'
test
"
log 'Launching simulator'
exec "xcrun simctl launch #{device} #{getBundleId name}"
catch {message}
logErr message
openXcode = (name) ->
try
exec "open native/ios/#{name}.xcworkspace"
catch {message}
logErr \
if message.match /ENOENT/i
"""
Cannot find #{name}.xcworkspace in native/ios.
Run this command from your project's root directory.
"""
else if message.match /EACCES/i
"Invalid permissions for opening #{name}.xcworkspace in native/ios"
else
message
getDeviceList = ->
try
exec 'xcrun instruments -s devices', true
.toString()
.split '\n'
.filter (line) -> /^i/.test line
catch {message}
logErr 'Device listing failed: ' + message
startRepl = (name) ->
log 'Starting REPL'
hasRlwrap =
try
exec 'type rlwrap'
true
catch
log '
Warning: rlwrap is not installed.\nInstall it to make the REPL a much
better experience with arrow key support.
', 'red'
false
try
lein = child.spawn (if hasRlwrap then 'rlwrap' else 'lein'),
"#{if hasRlwrap then 'lein ' else ''}trampoline run -m clojure.main -e"
.split(' ').concat(
"""
(require '[cljs.repl :as repl])
(require '[ambly.core :as ambly])
(let [repl-env (ambly.core/repl-env)]
(cljs.repl/repl repl-env
:watch \"src\"
:watch-fn
(fn []
(cljs.repl/load-file repl-env
\"src/#{toUnderscored name}/core.cljs\"))
:analyze-path \"src\"))
"""),
cwd: process.cwd()
env: process.env
stdio: 'inherit'
catch {message}
logErr message
cli._name = 'natal'
cli.version pkgJson.version
cli.command 'init <name>'
.description 'Create a new ClojureScript React Native project'
.action (name) ->
if typeof name isnt 'string'
logErr '''
natal init requires a project name as the first argument.
e.g.
natal init HelloWorld
'''
init name
cli.command 'launch'
.description 'Compile project and run in simulator'
.action ->
launch readConfig()
cli.command 'repl'
.description 'Launch a ClojureScript REPL with background compilation'
.action ->
startRepl readConfig().name
cli.command 'listdevices'
.description 'List available simulator devices by index'
.action ->
console.log (getDeviceList()
.map (line, i) -> "#{i}\t#{line.replace /\[.+\]/, ''}"
.join '\n')
cli.command 'setdevice <index>'
.description 'Choose simulator device by index'
.action (index) ->
unless device = getDeviceList()[parseInt index, 10]
logErr 'Invalid device index. Run natal listdevices for valid indexes.'
config = readConfig()
config.device = pluckUuid device
writeConfig config
cli.command 'xcode'
.description 'Open Xcode project'
.action ->
openXcode readConfig().name
cli.on '*', (command) ->
logErr "Unknown command #{command[0]}. See natal --help for valid commands"
unless semver.satisfies process.version[1...], nodeVersion
logErr """
Natal requires Node.js version #{nodeVersion}
You have #{process.version[1...]}
"""
if process.argv.length <= 2
cli.outputHelp()
else
cli.parse process.argv

View File

@ -1,6 +1,6 @@
{
"name": "natal",
"version": "0.0.3",
"version": "0.1.0",
"description": "Bootstrap ClojureScript React Native apps",
"main": "index.js",
"author": {
@ -12,12 +12,11 @@
"dependencies": {
"chalk": "^1.1.1",
"coffee-script": "^1.9.3",
"react-native": "^0.10.1",
"commander": "^2.8.1",
"semver": "^5.0.1"
},
"engines": {
"node": ">=0.12.x",
"iojs": ">=3.1.x"
"node": ">=4.0.0"
},
"repository": {
"type": "git",

View File

@ -97,7 +97,7 @@ RCT_EXPORT_MODULE()
* on the same Wi-Fi network.
*/
jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle"];
jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
/**
* OPTION 2
@ -131,7 +131,8 @@ RCT_EXPORT_MODULE()
// Set up a root view using the bridge defined above
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"$PROJECT_NAME$"];
moduleName:@"$PROJECT_NAME$"
initialProperties:nil];
// Set up to be notified when the React Native UI is up
[[NSNotificationCenter defaultCenter] addObserver:self

View File

@ -1,19 +0,0 @@
#!/bin/bash
if hash rlwrap 2>/dev/null; then
COMMAND="rlwrap lein"
else
COMMAND="lein"
fi
$COMMAND trampoline run -m clojure.main -e \
"(require '[cljs.repl :as repl])
(require '[ambly.core :as ambly])
(let [repl-env (ambly.core/repl-env)]
(cljs.repl/repl repl-env
:watch \"src\"
:watch-fn
(fn []
(cljs.repl/load-file repl-env
\"src/$PROJECT_NAME_UNDERSCORED$/core.cljs\"))
:analyze-path \"src\"))"

View File

@ -4,7 +4,7 @@
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.122"]
[org.clojure/clojurescript "1.7.145"]
[org.omcljs/om "0.9.0"]
[org.omcljs/ambly "0.6.0"]]
:plugins [[lein-cljsbuild "1.1.0"]]