re-natal/re-natal.coffee

980 lines
32 KiB
CoffeeScript
Raw Normal View History

# Re-Natal
2015-08-26 03:48:43 +00:00
# Bootstrap ClojureScript React Native apps
# Dan Motzenbecker
# http://oxism.com
# MIT License
2016-01-31 19:04:30 +00:00
fs = require 'fs-extra'
klawSync = require 'klaw-sync'
fpath = require 'path'
2015-10-17 19:10:31 +00:00
net = require 'net'
http = require 'http'
os = require 'os'
2015-10-04 22:53:54 +00:00
child = require 'child_process'
cli = require 'commander'
chalk = require 'chalk'
semver = require 'semver'
ckDeps = require 'check-dependencies'
2015-10-04 22:53:54 +00:00
pkgJson = require __dirname + '/package.json'
2015-08-22 03:45:42 +00:00
nodeVersion = pkgJson.engines.node
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
projNameUsRx = /\$PROJECT_NAME_UNDERSCORED\$/g
interfaceDepsRx = /\$INTERFACE_DEPS\$/g
platformRx = /\$PLATFORM\$/g
platformCleanRx = /#_\(\$PLATFORM_CLEAN\$\)/g
platformCleanId = "#_($PLATFORM_CLEAN$)"
devHostRx = /\$DEV_HOST\$/g
ipAddressRx = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/i
figwheelUrlRx = /ws:\/\/[0-9a-zA-Z\.]*:/g
appDelegateRx = /http:\/\/[^:]+/g
debugHostRx = /host]\s+\?:\s+@".*";/g
namespaceRx = /\(ns\s+([A-Za-z0-9.-]+)/g
jsRequireRx = /js\/require "(.+)"/g
rnVersion = '0.47.1'
rnWinVersion = '0.47.0-rc.5'
2015-10-17 19:10:31 +00:00
rnPackagerPort = 8081
process.title = 're-natal'
buildProfiles =
dev:
profilesRx: /#_\(\$DEV_PROFILES\$\)/g
profilesId: "#_($DEV_PROFILES$)"
prod:
profilesRx: /#_\(\$PROD_PROFILES\$\)/g
profilesId: "#_($PROD_PROFILES$)"
advanced:
profilesRx: /#_\(\$ADVANCED_PROFILES\$\)/g
profilesId: "#_($ADVANCED_PROFILES$)"
interfaceConf =
'reagent':
cljsDir: "cljs-reagent"
sources:
common: ["handlers.cljs", "subs.cljs", "db.cljs"]
other: []
deps: ['[reagent "0.5.1" :exclusions [cljsjs/react]]'
'[re-frame "0.6.0"]']
shims: ["cljsjs.react"]
sampleCommandNs: '(in-ns \'$PROJECT_NAME_HYPHENATED$.ios.core)'
sampleCommand: '(dispatch [:set-greeting "Hello Native World!"])'
'reagent6':
cljsDir: "cljs-reagent6"
sources:
common: ["events.cljs", "subs.cljs", "db.cljs"]
other: [["reagent_dom.cljs","reagent/dom.cljs"], ["reagent_dom_server.cljs","reagent/dom/server.cljs"]]
2017-07-14 18:37:33 +00:00
deps: ['[reagent "0.7.0" :exclusions [cljsjs/react cljsjs/react-dom cljsjs/react-dom-server cljsjs/create-react-class]]'
'[re-frame "0.9.2"]']
2017-07-14 18:37:33 +00:00
shims: ["cljsjs.react", "cljsjs.react.dom", "cljsjs.react.dom.server", "cljsjs.create-react-class"]
sampleCommandNs: '(in-ns \'$PROJECT_NAME_HYPHENATED$.ios.core)'
sampleCommand: '(dispatch [:set-greeting "Hello Native World!"])'
'om-next':
cljsDir: "cljs-om-next"
sources:
common: ["state.cljs"]
other: [["support.cljs","re_natal/support.cljs"]]
2017-05-27 22:56:14 +00:00
deps: ['[org.omcljs/om "1.0.0-beta1" :exclusions [cljsjs/react cljsjs/react-dom]]']
shims: ["cljsjs.react", "cljsjs.react.dom"]
sampleCommandNs: '(in-ns \'$PROJECT_NAME_HYPHENATED$.state)'
sampleCommand: '(swap! app-state assoc :app/msg "Hello Native World!")'
2016-04-27 03:12:01 +00:00
'rum':
cljsDir: "cljs-rum"
sources:
common: []
other: [["sablono_compiler.clj","sablono/compiler.clj"],["support.cljs","re_natal/support.cljs"]]
2017-03-07 17:18:37 +00:00
deps: ['[rum "0.10.8" :exclusions [cljsjs/react cljsjs/react-dom sablono]]']
2016-04-27 03:12:01 +00:00
shims: ["cljsjs.react", "cljsjs.react.dom", "sablono.core"]
sampleCommandNs: '(in-ns \'$PROJECT_NAME_HYPHENATED$.ios.core)'
sampleCommand: '(swap! app-state assoc :greeting "Hello Clojure in iOS and Android with Rum!")'
interfaceNames = Object.keys interfaceConf
defaultInterface = 'reagent6'
defaultEnvRoots =
dev: 'env/dev'
prod: 'env/prod'
platformMeta =
'ios':
name: "iOS"
sources: ["core.cljs"]
'android':
name: "Android"
sources: ["core.cljs"]
'windows':
name: "UWP"
sources: ["core.cljs"]
2017-04-12 13:47:59 +00:00
'wpf':
name: "WPF"
sources: ["core.cljs"]
platforms = []
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
2017-05-30 17:46:32 +00:00
child.execSync cmd, stdio: 'inherit'
2015-10-04 19:32:23 +00:00
else
child.execSync cmd, stdio: ['pipe', 'pipe', 'ignore']
2015-10-04 19:32:23 +00:00
ensureExecutableAvailable = (executable) ->
if os.platform() == 'win32'
try
exec "where #{executable}"
catch e
throw new Error("type: #{executable}: not found")
else
exec "type #{executable}"
2015-10-04 19:32:23 +00:00
2017-05-30 17:46:32 +00:00
isYarnAvailable = () ->
try
ensureExecutableAvailable('yarn')
true
catch e
false
isSomeDepsMissing = () ->
depState = ckDeps.sync {install: false, verbose: false}
!depState.depsWereOk
installDeps = (opts = verbose: false, report: false) ->
{verbose, report} = opts
if report
ckDeps.sync (install: false, verbose: true)
if isYarnAvailable()
exec 'yarn', verbose
else
exec 'npm i', verbose
ensureOSX = (cb) ->
if os.platform() == 'darwin'
cb()
else
logErr 'This command is only available on OSX'
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
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()
ensureXcode = (cb) ->
try
ensureExecutableAvailable 'xcodebuild'
cb();
catch {message}
if message.match /type.+xcodebuild/i
logErr 'Xcode Command Line Tools are required'
generateConfig = (interfaceName, projName) ->
log 'Creating Re-Natal config'
2015-11-14 23:18:19 +00:00
config =
name: projName
interface: interfaceName
2016-04-08 23:09:45 +00:00
envRoots: defaultEnvRoots
modules: []
imageDirs: ["images"]
platforms: {}
2015-11-14 23:18:19 +00:00
for platform in platforms
2017-05-30 17:46:32 +00:00
config.platforms[platform] =
host: "localhost"
modules: []
2015-11-14 23:18:19 +00:00
writeConfig config
config
2015-09-19 18:02:28 +00:00
writeConfig = (config) ->
try
fs.writeFileSync './.re-natal', JSON.stringify config, null, 2
2015-09-19 18:02:28 +00:00
catch {message}
logErr message
2015-09-19 18:02:28 +00:00
logErr \
if message.match /EACCES/i
'Invalid write permissions for creating .re-natal config file'
2015-09-19 18:02:28 +00:00
else
message
verifyConfig = (config) ->
if !config.platforms? || !config.modules? || !config.imageDirs? || !config.interface? || !config.envRoots?
throw new Error 're-natal project needs to be upgraded, please run: re-natal upgrade'
config
2015-09-19 18:02:28 +00:00
readConfig = (verify = true)->
2015-09-19 18:21:19 +00:00
try
config = JSON.parse readFile '.re-natal'
if (verify)
verifyConfig(config)
else
config
2015-09-19 18:21:19 +00:00
catch {message}
logErr \
if message.match /ENOENT/i
'No Re-Natal config was found in this directory (.re-natal)'
2015-09-19 18:21:19 +00:00
else if message.match /EACCES/i
'No read permissions for .re-natal'
2015-09-19 18:21:19 +00:00
else if message.match /Unexpected/i
'.re-natal contains malformed JSON'
2015-09-19 18:21:19 +00:00
else
message
scanImageDir = (dir) ->
fnames = fs.readdirSync(dir)
.map (fname) -> "#{dir}/#{fname}"
.filter (path) -> fs.statSync(path).isFile()
.filter (path) -> removeExcludeFiles(path)
.map (path) -> path.replace /@2x|@3x/i, ''
.map (path) -> path.replace new RegExp(".(#{platforms.join('|')})" + fpath.extname(path) + "$", "i"), fpath.extname(path)
.filter (v, idx, slf) -> slf.indexOf(v) == idx
dirs = fs.readdirSync(dir)
.map (fname) -> "#{dir}/#{fname}"
.filter (path) -> fs.statSync(path).isDirectory()
fnames.concat scanImages(dirs)
removeExcludeFiles = (file) ->
excludedFileNames = [".DS_Store"]
res = excludedFileNames.map (ex) -> (file.indexOf ex) == -1
true in res
scanImages = (dirs) ->
imgs = []
for dir in dirs
imgs = imgs.concat(scanImageDir(dir));
imgs
2015-10-04 00:35:46 +00:00
resolveAndroidDevHost = (deviceType) ->
allowedTypes = {'real': 'localhost', 'avd': '10.0.2.2', 'genymotion': '10.0.3.2'}
devHost = allowedTypes[deviceType]
if (devHost?)
log "Using '#{devHost}' for device type #{deviceType}"
devHost
else
deviceTypeIsIpAddress(deviceType, Object.keys(allowedTypes))
configureDevHostForAndroidDevice = (deviceType) ->
try
devHost = resolveAndroidDevHost(deviceType)
config = readConfig()
config.platforms.android.host = devHost
writeConfig(config)
2016-09-11 11:36:20 +00:00
log "Please run: re-natal use-figwheel to take effect."
catch {message}
logErr message
resolveIosDevHost = (deviceType) ->
if deviceType == 'simulator'
log "Using 'localhost' for iOS simulator"
'localhost'
else if deviceType == 'real'
en0Ip = exec('ipconfig getifaddr en0').toString().trim()
log "Using IP of interface en0:'#{en0Ip}' for real iOS device"
en0Ip
else
deviceTypeIsIpAddress(deviceType, ['simulator', 'real'])
configureDevHostForIosDevice = (deviceType) ->
try
devHost = resolveIosDevHost(deviceType)
config = readConfig()
config.platforms.ios.host = devHost
writeConfig(config)
2016-09-11 11:36:20 +00:00
log "Please run: re-natal use-figwheel to take effect."
catch {message}
logErr message
deviceTypeIsIpAddress = (deviceType, allowedTypes) ->
if deviceType.match(ipAddressRx)
log "Using development host IP: '#{deviceType}'"
deviceType
else
log("Value '#{deviceType}' is not a valid IP address, still configured it as development host. Did you mean one of: [#{allowedTypes}] ?", 'yellow')
deviceType
copyDevEnvironmentFilesForPlatform = (platform, interfaceName, projNameHyph, projName, devEnvRoot, devHost) ->
cljsDir = interfaceConf[interfaceName].cljsDir
fs.mkdirpSync "#{devEnvRoot}/env/#{platform}"
mainDevPath = "#{devEnvRoot}/env/#{platform}/main.cljs"
fs.copySync("#{resources}/#{cljsDir}/main_dev.cljs", mainDevPath)
edit mainDevPath, [[projNameHyphRx, projNameHyph], [projNameRx, projName], [platformRx, platform], [devHostRx, devHost]]
copyDevEnvironmentFiles = (interfaceName, projNameHyph, projName, devEnvRoot, devHost) ->
userNsPath = "#{devEnvRoot}/user.clj"
fs.copySync("#{resources}/user.clj", userNsPath)
for platform in platforms
copyDevEnvironmentFilesForPlatform platform, interfaceName, projNameHyph, projName, devEnvRoot, devHost
copyProdEnvironmentFilesForPlatform = (platform, interfaceName, projNameHyph, projName, prodEnvRoot) ->
cljsDir = interfaceConf[interfaceName].cljsDir
fs.mkdirpSync "#{prodEnvRoot}/env/#{platform}"
mainProdPath = "#{prodEnvRoot}/env/#{platform}/main.cljs"
fs.copySync("#{resources}/#{cljsDir}/main_prod.cljs", mainProdPath)
edit mainProdPath, [[projNameHyphRx, projNameHyph], [projNameRx, projName], [platformRx, platform]]
copyProdEnvironmentFiles = (interfaceName, projNameHyph, projName, prodEnvRoot) ->
for platform in platforms
copyProdEnvironmentFilesForPlatform platform, interfaceName, projNameHyph, projName, prodEnvRoot
copyFigwheelBridge = (projNameUs) ->
fs.copySync("#{resources}/figwheel-bridge.js", "./figwheel-bridge.js")
edit "figwheel-bridge.js", [[projNameUsRx, projNameUs]]
updateGitIgnore = () ->
fs.appendFileSync(".gitignore", "\n# Generated by re-natal\n#\n")
indexFiles = platforms.map (platform) -> "index.#{platform}.js"
fs.appendFileSync(".gitignore", indexFiles.join("\n"))
2017-04-13 09:27:05 +00:00
fs.appendFileSync(".gitignore", "\ntarget/\n")
fs.appendFileSync(".gitignore", "\n# Figwheel\n#\nfigwheel_server.log")
findPackagerFileToPatch = () ->
files = [
"node_modules/metro-bundler/src/Server/index.js",
"node_modules/metro-bundler/build/Server/index.js",
"node_modules/react-native/packager/src/Server/index.js"]
fileToPatch = files[0];
for f in files
if fs.existsSync(f)
fileToPatch = f
fileToPatch
patchReactNativePackager = () ->
2017-05-30 17:46:32 +00:00
installDeps()
fileToPatch = findPackagerFileToPatch()
log "Patching file #{fileToPatch} to serve *.map files."
edit fileToPatch,
[[/match.*\.map\$\/\)/m, "match(/index\\..*\\.map$/)"]]
log "If the React Native packager is running, please restart it."
shimCljsNamespace = (ns) ->
filePath = "src/" + ns.replace(/\./g, "/") + ".cljs"
2017-07-14 18:37:33 +00:00
filePath = filePath.replace(/-/g, "_")
fs.mkdirpSync fpath.dirname(filePath)
fs.writeFileSync(filePath, "(ns #{ns})")
copySrcFilesForPlatform = (platform, interfaceName, projName, projNameUs, projNameHyph) ->
cljsDir = interfaceConf[interfaceName].cljsDir
fs.mkdirSync "src/#{projNameUs}/#{platform}"
fileNames = platformMeta[platform].sources
for fileName in fileNames
path = "src/#{projNameUs}/#{platform}/#{fileName}"
fs.copySync("#{resources}/#{cljsDir}/#{fileName}", path)
edit path, [[projNameHyphRx, projNameHyph], [projNameRx, projName], [platformRx, platform]]
copySrcFiles = (interfaceName, projName, projNameUs, projNameHyph) ->
cljsDir = interfaceConf[interfaceName].cljsDir
fileNames = interfaceConf[interfaceName].sources.common;
for fileName in fileNames
path = "src/#{projNameUs}/#{fileName}"
fs.copySync("#{resources}/#{cljsDir}/#{fileName}", path)
edit path, [[projNameHyphRx, projNameHyph], [projNameRx, projName]]
for platform in platforms
copySrcFilesForPlatform platform, interfaceName, projName, projNameUs, projNameHyph
otherFiles = interfaceConf[interfaceName].sources.other;
for cpFile in otherFiles
from = "#{resources}/#{cljsDir}/#{cpFile[0]}"
to = "src/#{cpFile[1]}"
fs.copySync(from, to)
shims = fileNames = interfaceConf[interfaceName].shims;
for namespace in shims
shimCljsNamespace(namespace)
creteBuildConfigs = (profiles, platforms) ->
builds = {}
for profile in profiles
template = readFile "#{resources}/#{profile}.profile"
configs = platforms.map (platform) -> template.replace(platformRx, platform)
configs.push buildProfiles[profile].profilesId
builds[profile] = configs.join("\n")
builds
copyProjectClj = (interfaceName, projNameHyph) ->
fs.copySync("#{resources}/project.clj", "project.clj")
deps = interfaceConf[interfaceName].deps.join("\n")
cleans = platforms.map (platform) -> "\"index.#{platform}.js\""
cleans.push platformCleanId
builds = creteBuildConfigs ['dev', 'prod', 'advanced'], platforms
2017-07-16 08:22:57 +00:00
edit 'project.clj', [
[projNameHyphRx, projNameHyph],
[interfaceDepsRx, deps],
[platformCleanRx, cleans.join(' ')],
[buildProfiles.dev.profilesRx, builds.dev],
[buildProfiles.prod.profilesRx, builds.prod],
[buildProfiles.advanced.profilesRx, builds.advanced]]
updateProjectClj = (platform) ->
proj = readFile('project.clj')
cleans = []
cleans.push "\"index.#{platform}.js\""
cleans.push platformCleanId
if !proj.match(platformCleanRx)
log "Manual update of project.clj required: add clean targets:"
log "#{cleans.join(' ')}", "red"
builds = creteBuildConfigs ['dev', 'prod', 'advanced'], [platform]
2017-07-16 08:22:57 +00:00
profileKeys = Object.keys buildProfiles
for key in profileKeys
if !proj.match(buildProfiles[key].profilesRx)
log "Manual update of project.clj required: add new build to #{key} profile:"
log "#{builds[key]}", "red"
2017-07-16 08:22:57 +00:00
edit 'project.clj', [
[platformCleanRx, cleans.join(' ')],
[buildProfiles.dev.profilesRx, builds.dev],
[buildProfiles.prod.profilesRx, builds.prod],
[buildProfiles.advanced.profilesRx, builds.advanced]
2017-07-16 08:22:57 +00:00
]
init = (interfaceName, projName) ->
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'
2017-05-30 17:46:32 +00:00
if isYarnAvailable()
log '\u2615 Grab a coffee! I will use yarn, but fetching deps still takes time...', 'yellow'
else
log '\u2615 Grab a coffee! Downloading deps might take a while...', 'yellow'
2015-08-26 01:44:11 +00:00
if fs.existsSync projNameHyph
2015-09-19 03:10:03 +00:00
throw new Error "Directory #{projNameHyph} already exists"
ensureExecutableAvailable 'lein'
2015-10-03 19:48:28 +00:00
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
2016-01-31 19:04:30 +00:00
fs.removeSync "resources"
2015-09-19 03:10:03 +00:00
corePath = "src/#{projNameUs}/core.clj"
2015-08-22 15:06:30 +00:00
fs.unlinkSync corePath
copyProjectClj(interfaceName, projNameHyph)
2017-05-30 17:46:32 +00:00
copySrcFiles(interfaceName, projName, projNameUs, projNameHyph)
copyDevEnvironmentFiles(interfaceName, projNameHyph, projName, defaultEnvRoots.dev, "localhost")
copyProdEnvironmentFiles(interfaceName, projNameHyph, projName, defaultEnvRoots.prod)
fs.copySync("#{resources}/images", "./images")
2016-09-11 11:36:20 +00:00
log 'Creating React Native skeleton.'
2015-10-04 21:54:48 +00:00
2017-04-12 11:02:25 +00:00
pkg =
name: projName
version: '0.0.1'
private: true
scripts:
2017-07-11 19:30:39 +00:00
start: 'node node_modules/react-native/local-cli/cli.js start'
dependencies:
'react-native': rnVersion
# Fixes issue with packager 'TimeoutError: transforming ... took longer than 301 seconds.'
'babel-plugin-transform-es2015-block-scoping': '6.15.0'
2017-04-12 11:02:25 +00:00
2017-04-12 11:50:48 +00:00
if 'windows' in platforms || 'wpf' in platforms
2017-04-29 19:31:25 +00:00
pkg.dependencies['react-native-windows'] = rnWinVersion
2017-04-12 11:02:25 +00:00
fs.writeFileSync 'package.json', JSON.stringify pkg, null, 2
2017-05-30 17:46:32 +00:00
installDeps()
fs.unlinkSync '.gitignore'
exec "node -e
2015-11-22 17:17:45 +00:00
\"require('react-native/local-cli/cli').init('.', '#{projName}')\"
"
2015-11-20 21:43:47 +00:00
2017-04-12 11:02:25 +00:00
if 'windows' in platforms
log 'Creating React Native UWP project.'
2017-04-12 11:02:25 +00:00
exec "node -e
\"require('react-native-windows/local-cli/generate-windows')('.', '#{projName}', '#{projName}')\"
"
2017-04-12 11:50:48 +00:00
if 'wpf' in platforms
log 'Creating React Native WPF project.'
2017-04-12 11:50:48 +00:00
exec "node -e
\"require('react-native-windows/local-cli/generate-wpf')('.', '#{projName}', '#{projName}')\"
"
updateGitIgnore()
generateConfig(interfaceName, projName)
copyFigwheelBridge(projNameUs)
2015-08-22 17:30:42 +00:00
log 'Compiling ClojureScript'
exec 'lein prod-build'
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 ''
2016-09-11 12:50:57 +00:00
log 'Run iOS app:' , 'yellow'
log 'react-native run-ios > /dev/null', 'inverse'
log ''
log 'To use figwheel type:' , 'yellow'
log 're-natal use-figwheel', 'inverse'
log 'lein figwheel ios', 'inverse'
log ''
2016-09-11 12:50:57 +00:00
log 'Reload the app in simulator (\u2318 + R)'
2015-10-04 23:05:04 +00:00
log ''
2015-08-26 01:44:11 +00:00
log 'At the REPL prompt type this:', 'yellow'
log interfaceConf[interfaceName].sampleCommandNs.replace(projNameHyphRx, projNameHyph), '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 interfaceConf[interfaceName].sampleCommand, '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
'Leiningen is required (http://leiningen.org)'
else if message.match /npm/i
"npm install failed. This may be a network issue. Check #{projNameHyph}/npm-debug.log for details."
2015-09-19 03:05:39 +00:00
else
message
addPlatform = (platform) ->
try
if !(platform of platformMeta)
throw new Error "Unknown platform [#{platform}]"
config = readConfig()
platforms = Object.keys config.platforms
if platform in platforms
throw new Error "A project for a #{platformMeta[platform].name} app already exists"
else
interfaceName = config.interface
projName = config.name
projNameHyph = projName.replace(camelRx, '$1-$2').toLowerCase()
projNameUs = toUnderscored projName
log "Preparing for #{platformMeta[platform].name} app."
updateProjectClj(platform)
copySrcFilesForPlatform(platform, interfaceName, projName, projNameUs, projNameHyph)
copyDevEnvironmentFilesForPlatform(platform, interfaceName, projNameHyph, projName, defaultEnvRoots.dev, "localhost")
copyProdEnvironmentFilesForPlatform(platform, interfaceName, projNameHyph, projName, defaultEnvRoots.prod)
pkg = JSON.parse readFile 'package.json'
unless 'react-native-windows' in pkg.dependencies
pkg.dependencies['react-native-windows'] = rnWinVersion
fs.writeFileSync 'package.json', JSON.stringify pkg, null, 2
2017-05-30 17:46:32 +00:00
installDeps()
if platform is 'windows'
log 'Creating React Native UWP project.'
exec "node -e
\"require('react-native-windows/local-cli/generate-windows')('.', '#{projName}', '#{projName}')\"
"
if platform is 'wpf'
log 'Creating React Native WPF project.'
exec "node -e
\"require('react-native-windows/local-cli/generate-wpf')('.', '#{projName}', '#{projName}')\"
"
fs.appendFileSync(".gitignore", "\n\nindex.#{platform}.js\n")
config.platforms[platform] =
host: "localhost"
modules: []
writeConfig(config)
log 'Compiling ClojureScript'
exec 'lein prod-build'
catch {message}
logErr message
2015-09-19 18:47:28 +00:00
openXcode = (name) ->
try
exec "open ios/#{name}.xcodeproj"
2015-09-19 18:47:28 +00:00
catch {message}
logErr \
if message.match /ENOENT/i
"""
Cannot find #{name}.xcodeproj in ios.
2015-09-19 18:47:28 +00:00
Run this command from your project's root directory.
"""
else if message.match /EACCES/i
"Invalid permissions for opening #{name}.xcodeproj in ios"
2015-09-19 18:47:28 +00:00
else
message
2015-08-26 03:48:43 +00:00
generateRequireModulesCode = (modules) ->
2017-07-14 18:37:33 +00:00
jsCode = "var modules={'react-native': require('react-native'), 'react': require('react'), 'create-react-class': require('create-react-class')};"
for m in modules
jsCode += "modules['#{m}']=require('#{m}');";
jsCode += '\n'
updateFigwheelUrls = (devEnvRoot, devHost) ->
for platform in platforms
mainDevPath = "#{devEnvRoot}/env/#{platform}/main.cljs"
edit mainDevPath, [[figwheelUrlRx, "ws://#{devHost[platform]}:"]]
# Current RN version (0.29.2) has no host in AppDelegate.m maybe docs are outdated?
updateIosAppDelegate = (projName, iosHost) ->
appDelegatePath = "ios/#{projName}/AppDelegate.m"
edit appDelegatePath, [[appDelegateRx, "http://#{iosHost}"]]
2016-05-04 16:15:08 +00:00
updateIosRCTWebSocketExecutor = (iosHost) ->
RCTWebSocketExecutorPath = "node_modules/react-native/Libraries/WebSocket/RCTWebSocketExecutor.m"
edit RCTWebSocketExecutorPath, [[debugHostRx, "host] ?: @\"#{iosHost}\";"]]
platformOfNamespace = (ns) ->
if ns?
possiblePlatforms = Object.keys platformMeta
p = possiblePlatforms.find((p) -> ns.indexOf(".#{p}") > 0);
p ?= "common"
extractRequiresFromSourceFile = (file) ->
content = fs.readFileSync(file, encoding: 'utf8')
requires = []
while match = namespaceRx.exec(content)
ns = match[1]
while match = jsRequireRx.exec(content)
requires.push(match[1])
platform: platformOfNamespace(ns)
requires: requires
buildRequireByPlatformMap = () ->
onlyUserCljs = (item) -> fpath.extname(item.path) == '.cljs' and
item.path.indexOf('/target/') < 0 # ignore target dir
files = klawSync process.cwd(),
nodir: true
filter: onlyUserCljs
filenames = files.map((o) -> o.path)
extractedRequires = filenames.map(extractRequiresFromSourceFile)
extractedRequires.reduce((result, item) ->
platform = item.platform
if result[platform]?
result[platform] = Array.from(new Set(item.requires.concat(result[platform])))
else
result[platform] = Array.from(new Set(item.requires))
result
, {})
platformModulesAndImages = (config, platform) ->
if config.autoRequire? and config.autoRequire
requires = buildRequireByPlatformMap()
requires.common.concat(requires[platform])
else
images = scanImages(config.imageDirs).map (fname) -> './' + fname;
modulesAndImages = config.modules.concat images;
if typeof config.platforms[platform].modules is 'undefined'
modulesAndImages
else
modulesAndImages.concat(config.platforms[platform].modules)
generateDevScripts = () ->
try
config = readConfig()
platforms = Object.keys config.platforms
projName = config.name
devEnvRoot = config.envRoots.dev
2017-05-30 17:46:32 +00:00
if isSomeDepsMissing()
2017-05-30 17:52:54 +00:00
installDeps(verbose: true)
log 'Cleaning...'
exec 'lein clean'
devHost = {}
for platform in platforms
devHost[platform] = config.platforms[platform].host
if config.autoRequire? and config.autoRequire
log 'Auto-require is enabled. Scanning for require() calls in *.cljs files...'
for platform in platforms
moduleMap = generateRequireModulesCode(platformModulesAndImages(config, platform))
fs.writeFileSync "index.#{platform}.js", "#{moduleMap}require('figwheel-bridge').withModules(modules).start('#{projName}','#{platform}','#{devHost[platform]}');"
log "index.#{platform}.js was regenerated"
#updateIosAppDelegate(projName, devHost.ios)
updateIosRCTWebSocketExecutor(devHost.ios)
log "Host in RCTWebSocketExecutor.m was updated"
updateFigwheelUrls(devEnvRoot, devHost)
for platform in platforms
log "Dev server host for #{platformMeta[platform].name}: #{devHost[platform]}"
catch {message}
logErr \
if message.match /EACCES/i
'Invalid write permissions for creating development scripts'
else
message
doUpgrade = (config) ->
projName = config.name
projNameHyph = projName.replace(camelRx, '$1-$2').toLowerCase()
projNameUs = toUnderscored projName
unless config.interface
config.interface = defaultInterface
2015-10-04 22:53:54 +00:00
unless config.modules
config.modules = []
unless config.imageDirs
config.imageDirs = ["images"]
unless config.envRoots
config.envRoots = defaultEnvRoots
unless config.platforms
config.platforms =
ios:
host: "localhost"
modules: []
android:
host: "localhost"
modules: []
if config.iosHost?
config.platforms.ios.host = config.iosHost
delete config.iosHost
if config.androidHost?
config.platforms.android.host = config.androidHost
delete config.androidHost
if config.modulesPlatform?
if config.modulesPlatform.ios?
config.platforms.ios.modules = config.platforms.ios.modules.concat(config.modulesPlatform.ios)
if config.modulesPlatform.android?
config.platforms.android.modules = config.platforms.android.modules.concat(config.modulesPlatform.android)
delete config.modulesPlatform
writeConfig(config)
log 'upgraded .re-natal'
interfaceName = config.interface
envRoots = config.envRoots
copyDevEnvironmentFiles(interfaceName, projNameHyph, projName, envRoots.dev, "localhost")
copyProdEnvironmentFiles(interfaceName, projNameHyph, projName, envRoots.prod)
log "upgraded files in #{envRoots.dev} and #{envRoots.prod} "
copyFigwheelBridge(projNameUs)
log 'upgraded figwheel-bridge.js'
log('To upgrade React Native version please follow the official guide in https://facebook.github.io/react-native/docs/upgrading.html', 'yellow')
useComponent = (name, platform) ->
try
config = readConfig()
platforms = Object.keys config.platforms
if typeof platform isnt 'string'
config.modules.push name
log "Component '#{name}' is now configured for figwheel, please re-run 'use-figwheel' command to take effect"
else if platforms.indexOf(platform) > -1
if typeof config.platforms[platform].modules is 'undefined'
config.platforms[platform].modules = []
config.platforms[platform].modules.push name
log "Component '#{name}' (#{platform}-only) is now configured for figwheel, please re-run 'use-figwheel' command to take effect"
else
throw new Error("unsupported platform: #{platform}")
writeConfig(config)
catch {message}
logErr message
inferComponents = () ->
requiresByPlatform = buildRequireByPlatformMap()
allRequires = []
for k,v of requiresByPlatform
allRequires = Array.from(new Set(allRequires.concat(v)))
config = readConfig() # re-natal file
modules = new Set(config.modules)
difference = new Set(Array.from(allRequires).filter((m) -> !modules.has(m)))
if(difference.size isnt 0)
log "new component import found #{Array.from(difference)}"
config.modules = Array.from(allRequires)
writeConfig(config)
else
log "no new component was imported, defaulting to #{Array.from(modules)}"
autoRequire = (enabled) ->
config = readConfig()
config.autoRequire = enabled
writeConfig(config)
if (enabled)
log "Auto-Require feature is enabled in use-figwheel command"
else
log "Auto-Require feature is disabled in use-figwheel command"
cli._name = 're-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'
.option "-i, --interface [#{interfaceNames.join ' '}]", 'specify React interface', defaultInterface
.option '-u, --uwp', 'create project for UWP app'
.option '-w, --wpf', 'create project for WPF app'
.action (name, cmd) ->
2015-09-19 17:51:11 +00:00
if typeof name isnt 'string'
logErr '''
re-natal init requires a project name as the first argument.
2015-09-19 17:51:11 +00:00
e.g.
re-natal init HelloWorld
2015-09-19 17:51:11 +00:00
'''
unless interfaceConf[cmd.interface]
logErr "Unsupported React interface: #{cmd.interface}, one of [#{interfaceNames}] was expected."
platforms.push 'ios'
platforms.push 'android'
2017-04-12 11:02:25 +00:00
if cmd.uwp?
platforms.push 'windows'
2017-04-12 11:50:48 +00:00
if cmd.wpf?
platforms.push 'wpf'
ensureFreePort -> init(cmd.interface, name)
2015-09-19 17:51:11 +00:00
cli.command 'upgrade'
.description 'upgrades project files to current installed version of re-natal (the upgrade of re-natal itself is done via npm)'
.action ->
doUpgrade readConfig(false)
cli.command 'add-platform <platform>'
.description 'adds additional app platform: \'windows\' - UWP app, \'wpf\' - WPF app'
.action (platform) ->
addPlatform(platform)
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 ->
ensureOSX ->
ensureXcode ->
openXcode readConfig().name
2015-10-04 19:19:04 +00:00
cli.command 'deps'
.description 'install all dependencies for the project'
.action ->
2017-05-30 17:46:32 +00:00
installDeps(verbose: true, report: true)
cli.command 'use-figwheel'
.description 'generate index.*.js for development with figwheel'
.action () ->
generateDevScripts()
cli.command 'use-android-device <type>'
.description 'sets up the host for android device type: \'real\' - localhost, \'avd\' - 10.0.2.2, \'genymotion\' - 10.0.3.2, IP'
.action (type) ->
configureDevHostForAndroidDevice type
cli.command 'use-ios-device <type>'
2016-05-15 00:09:41 +00:00
.description 'sets up the host for ios device type: \'simulator\' - localhost, \'real\' - auto detect IP on eth0, IP'
.action (type) ->
configureDevHostForIosDevice type
cli.command 'use-component <name> [<platform>]'
.description 'configures a custom component to work with figwheel. name is the value you pass to (js/require) function.'
.action (name, platform) ->
useComponent(name, platform)
cli.command 'infer-components'
.description 'parses all cljs files in this project, extracts all (js/require) components calls and uses them to populate the re-natal file'
.action () ->
inferComponents()
cli.command 'enable-source-maps'
.description 'patches RN packager to server *.map files from filesystem, so that chrome can download them.'
.action () ->
patchReactNativePackager()
cli.command 'enable-auto-require'
2017-09-24 21:16:53 +00:00
.description 'enables source scanning for automatic required module resolution in use-figwheel command.'
.action () ->
autoRequire(true)
cli.command 'disable-auto-require'
2017-09-24 21:16:53 +00:00
.description 'disables auto-require feature in use-figwheel command'
.action () ->
autoRequire(false)
cli.command 'copy-figwheel-bridge'
.description 'copy figwheel-bridge.js into project'
.action () ->
copyFigwheelBridge(readConfig(false).name)
log "Copied figwheel-bridge.js"
2015-10-04 02:57:22 +00:00
cli.on '*', (command) ->
logErr "unknown command #{command[0]}. See re-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 """
Re-Natal requires Node.js version #{nodeVersion}
You have #{process.version[1...]}
"""
if process.argv.length <= 2
cli.outputHelp()
else
cli.parse process.argv