re-natal/natal.coffee

538 lines
14 KiB
CoffeeScript
Raw Normal View History

2015-08-26 03:48:43 +00:00
# Natal
# Bootstrap ClojureScript React Native apps
# Dan Motzenbecker
# http://oxism.com
# MIT License
2015-10-04 22:53:54 +00:00
fs = require 'fs'
2015-10-17 19:10:31 +00:00
net = require 'net'
http = require 'http'
2015-10-04 22:53:54 +00:00
crypto = require 'crypto'
child = require 'child_process'
cli = require 'commander'
chalk = require 'chalk'
semver = require 'semver'
pkgJson = require __dirname + '/package.json'
2015-08-22 03:45:42 +00:00
nodeVersion = pkgJson.engines.node
2015-08-22 03:45:42 +00:00
resources = __dirname + '/resources/'
validNameRx = /^[A-Z][0-9A-Z]*$/i
2015-08-22 03:45:42 +00:00
camelRx = /([a-z])([A-Z])/g
projNameRx = /\$PROJECT_NAME\$/g
projNameHyphRx = /\$PROJECT_NAME_HYPHENATED\$/g
2015-10-24 01:54:54 +00:00
rnVersion = '0.13.0'
2015-10-17 19:10:31 +00:00
rnPackagerPort = 8081
2015-09-19 18:19:54 +00:00
podMinVersion = '0.38.2'
2015-10-04 19:09:11 +00:00
process.title = 'natal'
2015-10-31 17:56:27 +00:00
reactInterfaces =
2015-10-28 03:56:45 +00:00
om: 'org.omcljs/om "0.9.0"'
2015-10-31 18:22:20 +00:00
'om-next': 'org.omcljs/om "1.0.0-alpha14"'
2015-10-28 03:56:45 +00:00
2015-10-31 17:56:27 +00:00
interfaceNames = Object.keys reactInterfaces
defaultInterface = 'om'
sampleCommands =
om: '(swap! app-state assoc :text "Hello Native World")'
'om-next': '(swap! app-state assoc :app/msg "Hello Native World")'
2015-10-28 03:56:45 +00:00
2015-08-22 03:45:42 +00:00
2015-08-26 01:28:58 +00:00
log = (s, color = 'green') ->
console.log chalk[color] s
2015-08-22 03:45:42 +00:00
2015-08-26 01:28:58 +00:00
logErr = (err, color = 'red') ->
console.error chalk[color] err
2015-09-19 18:47:07 +00:00
process.exit 1
2015-08-22 03:45:42 +00:00
2015-10-04 19:32:23 +00:00
exec = (cmd, keepOutput) ->
if keepOutput
2015-10-04 22:53:54 +00:00
child.execSync cmd
2015-10-04 19:32:23 +00:00
else
2015-10-04 22:53:54 +00:00
child.execSync cmd, stdio: 'ignore'
2015-10-04 19:32:23 +00:00
2015-10-04 00:34:10 +00:00
readFile = (path) ->
fs.readFileSync path, encoding: 'ascii'
2015-10-04 19:33:20 +00:00
edit = (path, pairs) ->
2015-08-22 03:45:42 +00:00
fs.writeFileSync path, pairs.reduce (contents, [rx, replacement]) ->
contents.replace rx, replacement
2015-10-04 00:34:10 +00:00
, readFile path
2015-08-22 03:45:42 +00:00
pluckUuid = (line) ->
line.match(/\[(.+)\]/)[1]
2015-11-14 23:17:38 +00:00
getUuidForDevice = (deviceName) ->
device = getDeviceList().find (line) -> line.match deviceName
unless device
logErr "Cannot find device `#{deviceName}`"
pluckUuid device
toUnderscored = (s) ->
s.replace(camelRx, '$1_$2').toLowerCase()
2015-10-17 19:10:31 +00:00
checkPort = (port, cb) ->
sock = net.connect {port}, ->
sock.end()
2015-11-14 19:52:30 +00:00
http.get "http://localhost:#{port}/status", (res) ->
2015-10-17 19:10:31 +00:00
data = ''
res.on 'data', (chunk) -> data += chunk
res.on 'end', ->
cb data.toString() isnt 'packager-status:running'
.on 'error', -> cb true
.setTimeout 3000
sock.on 'error', ->
sock.end()
cb false
ensureFreePort = (cb) ->
checkPort rnPackagerPort, (inUse) ->
if inUse
logErr "
Port #{rnPackagerPort} is currently in use by another process
and is needed by the React Native packager.
"
cb()
2015-11-14 23:18:19 +00:00
generateConfig = (name) ->
log 'Creating Natal config'
config =
name: name
device: getUuidForDevice 'iPhone 6s'
writeConfig config
config
2015-09-19 18:02:28 +00:00
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
2015-09-19 18:21:19 +00:00
readConfig = ->
try
2015-10-04 00:34:10 +00:00
JSON.parse readFile '.natal'
2015-09-19 18:21:19 +00:00
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) ->
2015-10-04 00:35:46 +00:00
try
2015-10-04 23:44:22 +00:00
if line = readFile "native/ios/#{name}.xcodeproj/project.pbxproj"
.match /PRODUCT_BUNDLE_IDENTIFIER = (.+);/
2015-10-04 00:35:46 +00:00
line[1]
2015-10-04 23:44:22 +00:00
else if line = readFile "native/ios/#{name}/Info.plist"
.match /\<key\>CFBundleIdentifier\<\/key\>\n?\s*\<string\>(.+)\<\/string\>/
2015-10-04 00:35:46 +00:00
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
2015-10-31 17:56:27 +00:00
init = (projName, interfaceName) ->
if projName.toLowerCase() is 'react' or !projName.match validNameRx
logErr 'Invalid project name. Use an alphanumeric CamelCase name.'
projNameHyph = projName.replace(camelRx, '$1-$2').toLowerCase()
projNameUs = toUnderscored projName
try
2015-09-19 03:10:03 +00:00
log "Creating #{projName}", 'bgMagenta'
2015-08-26 01:44:11 +00:00
log ''
if fs.existsSync projNameHyph
2015-09-19 03:10:03 +00:00
throw new Error "Directory #{projNameHyph} already exists"
2015-10-04 19:32:23 +00:00
exec 'type lein'
exec 'type pod'
exec 'type watchman'
2015-10-04 23:33:37 +00:00
exec 'type xcodebuild'
2015-10-03 19:48:28 +00:00
2015-10-04 19:32:23 +00:00
podVersion = exec('pod --version', true).toString().trim()
2015-09-19 03:10:03 +00:00
unless semver.satisfies podVersion, ">=#{podMinVersion}"
2015-09-19 02:55:59 +00:00
throw new Error """
2015-09-19 03:10:03 +00:00
Natal requires CocoaPods #{podMinVersion} or higher (you have #{podVersion}).
2015-09-19 02:55:59 +00:00
Run [sudo] gem update cocoapods and try again.
"""
2015-08-22 15:06:30 +00:00
log 'Creating Leiningen project'
2015-10-04 19:32:23 +00:00
exec "lein new #{projNameHyph}"
2015-08-22 15:06:30 +00:00
log 'Updating Leiningen project'
process.chdir projNameHyph
2015-10-04 19:32:23 +00:00
exec "cp #{resources}project.clj project.clj"
2015-10-31 15:35:07 +00:00
edit \
'project.clj',
2015-10-31 17:56:27 +00:00
[
[projNameHyphRx, projNameHyph]
[/\$REACT_INTERFACE\$/, reactInterfaces[interfaceName]]
]
2015-10-31 15:35:07 +00:00
2015-09-19 03:10:03 +00:00
corePath = "src/#{projNameUs}/core.clj"
2015-08-22 15:06:30 +00:00
fs.unlinkSync corePath
corePath += 's'
2015-10-31 17:56:27 +00:00
exec "cp #{resources}#{interfaceName}.cljs #{corePath}"
2015-10-04 19:33:20 +00:00
edit corePath, [[projNameHyphRx, projNameHyph], [projNameRx, projName]]
2015-08-22 17:30:42 +00:00
log 'Creating React Native skeleton'
2015-10-04 21:54:48 +00:00
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
2015-10-04 21:54:48 +00:00
2015-10-04 19:32:23 +00:00
exec 'npm i'
2015-10-04 21:54:48 +00:00
exec "
node -e
\"process.argv[3]='#{projName}';
require('react-native/local-cli/init')('.', '#{projName}')\"
"
exec 'rm -rf android'
fs.unlinkSync 'index.android.js'
2015-08-22 17:30:42 +00:00
2015-08-22 17:32:06 +00:00
log 'Installing Pod dependencies'
2015-10-04 21:54:48 +00:00
process.chdir 'ios'
2015-10-04 19:32:23 +00:00
exec "cp #{resources}Podfile ."
exec 'pod install'
2015-08-22 17:32:06 +00:00
2015-08-22 17:32:53 +00:00
log 'Updating Xcode project'
for ext in ['m', 'h']
2015-09-19 03:10:03 +00:00
path = "#{projName}/AppDelegate.#{ext}"
2015-10-04 19:32:23 +00:00
exec "cp #{resources}AppDelegate.#{ext} #{path}"
2015-10-04 19:33:20 +00:00
edit path, [[projNameRx, projName], [projNameHyphRx, projNameHyph]]
2015-08-22 17:32:53 +00:00
uuid1 = crypto
.createHash 'md5'
.update projName, 'utf8'
.digest('hex')[...24]
.toUpperCase()
2015-08-29 05:46:58 +00:00
uuid2 = uuid1.split ''
uuid2.splice 7, 1, ((parseInt(uuid1[7], 16) + 1) % 16).toString(16).toUpperCase()
uuid2 = uuid2.join ''
2015-08-22 17:32:53 +00:00
2015-10-04 19:33:20 +00:00
edit \
2015-09-19 03:10:03 +00:00
"#{projName}.xcodeproj/project.pbxproj",
2015-08-22 17:32:53 +00:00
[
[
/OTHER_LDFLAGS = "-ObjC";/g
'OTHER_LDFLAGS = "${inherited}";'
]
[
/\/\* End PBXBuildFile section \*\//
2015-09-19 03:10:03 +00:00
"\t\t#{uuid2} /* out in Resources */ =
{isa = PBXBuildFile; fileRef = #{uuid1} /* out */; };
2015-08-22 17:32:53 +00:00
\n/* End PBXBuildFile section */"
]
[
/\/\* End PBXFileReference section \*\//
2015-09-19 03:10:03 +00:00
"\t\t#{uuid1} /* out */ = {isa = PBXFileReference; lastKnownFileType
2015-10-04 21:54:48 +00:00
= folder; name = out; path = ../../target/out;
2015-08-22 17:32:53 +00:00
sourceTree = \"<group>\"; };\n/* End PBXFileReference section */"
]
[
/main.jsbundle \*\/\,/
2015-09-19 03:10:03 +00:00
"main.jsbundle */,\n\t\t\t\t#{uuid1} /* out */,"
2015-08-22 17:32:53 +00:00
]
[
/\/\* LaunchScreen.xib in Resources \*\/\,/
"/* LaunchScreen.xib in Resources */,
2015-09-19 03:10:03 +00:00
\n\t\t\t\t#{uuid2} /* out in Resources */,"
2015-08-22 17:32:53 +00:00
]
]
testId = readFile("#{projName}.xcodeproj/project.pbxproj")
.match(new RegExp "([0-9A-F]+) \/\\* #{projName}Tests \\*\/ = \\{")[1]
2015-10-04 19:33:20 +00:00
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>
"""
]
]
2015-09-19 03:10:03 +00:00
process.chdir '../..'
2015-11-14 23:18:19 +00:00
launch generateConfig projName
2015-10-04 23:05:04 +00:00
log ''
log 'To get started with your new app, first cd into its directory:', 'yellow'
2015-09-19 03:10:03 +00:00
log "cd #{projNameHyph}", 'inverse'
2015-10-04 23:05:04 +00:00
log ''
log 'Boot the REPL by typing:', 'yellow'
log 'natal repl', 'inverse'
log ''
2015-08-26 01:44:11 +00:00
log 'At the REPL prompt type this:', 'yellow'
2015-09-19 03:10:03 +00:00
log "(in-ns '#{projNameHyph}.core)", 'inverse'
2015-10-04 23:05:04 +00:00
log ''
2015-08-26 01:44:11 +00:00
log 'Changes you make via the REPL or by changing your .cljs files should appear live.', 'yellow'
2015-10-04 23:05:04 +00:00
log ''
2015-08-26 01:44:11 +00:00
log 'Try this command as an example:', 'yellow'
log sampleCommands[interfaceName], 'inverse'
log ''
2015-08-26 03:48:43 +00:00
log '✔ Done', 'bgMagenta'
log ''
2015-08-26 01:44:11 +00:00
2015-09-19 03:05:39 +00:00
catch {message}
logErr \
if message.match /type.+lein/i
2015-10-03 19:48:28 +00:00
'Leiningen is required (http://leiningen.org)'
else if message.match /type.+pod/i
2015-10-03 19:48:28 +00:00
'CocoaPods is required (https://cocoapods.org)'
else if message.match /type.+watchman/i
2015-10-03 19:48:28 +00:00
'Watchman is required (https://facebook.github.io/watchman)'
else if message.match /type.+xcodebuild/i
2015-10-04 23:33:37 +00:00
'Xcode Command Line Tools are required'
else if message.match /npm/i
"npm install failed. This may be a network issue. Check #{projNameHyph}/native/npm-debug.log for details."
2015-09-19 03:05:39 +00:00
else
message
2015-09-19 18:47:28 +00:00
launch = ({name, device}) ->
unless device in getDeviceUuids()
log 'Device ID not available, defaulting to iPhone 6s simulator', 'yellow'
{device} = generateConfig name
try
fs.statSync 'native/node_modules'
fs.statSync 'native/ios/Pods'
catch
logErr 'Dependencies are missing. Run natal deps to install them.'
log 'Compiling ClojureScript'
exec 'lein cljsbuild once dev'
2015-10-04 21:54:48 +00:00
log 'Compiling Xcode project'
try
2015-10-04 19:32:23 +00:00
exec "
xcodebuild
2015-10-04 21:54:48 +00:00
-workspace native/ios/#{name}.xcworkspace
2015-10-04 19:32:23 +00:00
-scheme #{name}
-destination platform='iOS Simulator',OS=latest,id='#{device}'
2015-10-16 01:36:58 +00:00
test
2015-10-04 19:32:23 +00:00
"
log 'Launching simulator'
2015-10-04 19:32:23 +00:00
exec "xcrun simctl launch #{device} #{getBundleId name}"
catch {message}
logErr message
2015-09-19 18:47:28 +00:00
openXcode = (name) ->
try
2015-10-04 21:54:48 +00:00
exec "open native/ios/#{name}.xcworkspace"
2015-09-19 18:47:28 +00:00
catch {message}
logErr \
if message.match /ENOENT/i
"""
2015-10-04 21:54:48 +00:00
Cannot find #{name}.xcworkspace in native/ios.
2015-09-19 18:47:28 +00:00
Run this command from your project's root directory.
"""
else if message.match /EACCES/i
2015-10-04 21:54:48 +00:00
"Invalid permissions for opening #{name}.xcworkspace in native/ios"
2015-09-19 18:47:28 +00:00
else
message
2015-08-26 03:48:43 +00:00
2015-09-19 19:35:00 +00:00
getDeviceList = ->
try
2015-10-04 19:32:23 +00:00
exec 'xcrun instruments -s devices', true
2015-09-19 19:35:00 +00:00
.toString()
.split '\n'
.filter (line) -> /^i/.test line
catch {message}
logErr 'Device listing failed: ' + message
getDeviceUuids = ->
getDeviceList().map (line) -> line.match(/\[(.+)\]/)[1]
startRepl = (name, autoChoose) ->
2015-10-04 22:53:54 +00:00
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
2015-11-14 19:52:30 +00:00
child.spawn (if hasRlwrap then 'rlwrap' else 'lein'),
2015-10-04 22:53:54 +00:00
"#{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#{if autoChoose then ' :choose-first-discovered true' else ''})]
2015-10-04 22:53:54 +00:00
(cljs.repl/repl repl-env
:watch \"src\"
:watch-fn
(fn []
(cljs.repl/load-file repl-env
\"src/#{toUnderscored name}/core.cljs\"))
2015-10-04 22:53:54 +00:00
:analyze-path \"src\"))
"""),
cwd: process.cwd()
env: process.env
stdio: 'inherit'
catch {message}
logErr message
2015-10-04 19:37:15 +00:00
cli._name = 'natal'
2015-10-04 01:18:26 +00:00
cli.version pkgJson.version
2015-08-26 03:48:43 +00:00
2015-09-19 17:51:11 +00:00
cli.command 'init <name>'
.description 'create a new ClojureScript React Native project'
2015-10-31 17:56:27 +00:00
.option "-i, --interface [#{interfaceNames.join ' '}]", 'specify React interface'
.action (name, cmd) ->
if cmd
2015-10-31 17:56:27 +00:00
interfaceName = cmd['interface'] or defaultInterface
else
2015-10-31 17:56:27 +00:00
interfaceName = defaultInterface
2015-10-31 17:56:27 +00:00
unless reactInterfaces[interfaceName]
logErr "Unsupported React interface: #{interfaceName}"
2015-09-19 17:51:11 +00:00
if typeof name isnt 'string'
logErr '''
natal init requires a project name as the first argument.
e.g.
natal init HelloWorld
'''
2015-08-26 03:48:43 +00:00
2015-10-31 17:56:27 +00:00
ensureFreePort -> init name, interfaceName
2015-09-19 17:51:11 +00:00
2015-10-04 19:19:04 +00:00
2015-09-19 19:35:00 +00:00
cli.command 'launch'
2015-10-30 03:57:09 +00:00
.description 'compile project and run in simulator'
2015-09-19 19:35:00 +00:00
.action ->
ensureFreePort -> launch readConfig()
2015-09-19 19:35:00 +00:00
2015-09-19 18:47:28 +00:00
2015-10-04 22:54:05 +00:00
cli.command 'repl'
2015-10-30 03:57:09 +00:00
.description 'launch a ClojureScript REPL with background compilation'
.option '-c, --choose', 'choose target device from list'
.action (cmd) ->
startRepl readConfig().name, !cmd.choose
2015-10-04 22:54:05 +00:00
2015-09-19 19:35:00 +00:00
cli.command 'listdevices'
2015-10-30 03:57:09 +00:00
.description 'list available simulator devices by index'
2015-09-19 19:35:00 +00:00
.action ->
console.log (getDeviceList()
2015-10-04 01:22:34 +00:00
.map (line, i) -> "#{i}\t#{line.replace /\[.+\]/, ''}"
2015-09-19 19:35:00 +00:00
.join '\n')
2015-10-04 19:19:04 +00:00
2015-10-04 02:36:46 +00:00
cli.command 'setdevice <index>'
2015-10-30 03:57:09 +00:00
.description 'choose simulator device by index'
2015-10-04 02:36:46 +00:00
.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
2015-10-04 19:19:04 +00:00
cli.command 'xcode'
2015-10-30 03:57:09 +00:00
.description 'open Xcode project'
2015-10-04 19:19:04 +00:00
.action ->
openXcode readConfig().name
cli.command 'deps'
.description 'install all dependencies for the project'
.action ->
try
process.chdir 'native'
log 'Installing npm packages'
exec 'npm i'
log 'Installing pods'
process.chdir 'ios'
exec 'pod install'
catch {message}
logErr message
2015-10-04 02:57:22 +00:00
cli.on '*', (command) ->
2015-10-30 03:57:09 +00:00
logErr "unknown command #{command[0]}. See natal --help for valid commands"
2015-10-04 02:57:22 +00:00
2015-09-19 17:51:11 +00:00
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