UNPKG

34.6 kBtext/coffeescriptView Raw
1# Re-Natal
2# Bootstrap ClojureScript React Native apps
3# Dan Motzenbecker
4# http://oxism.com
5# MIT License
6
7fs = require 'fs-extra'
8klawSync = require 'klaw-sync'
9fpath = require 'path'
10net = require 'net'
11http = require 'http'
12os = require 'os'
13child = require 'child_process'
14cli = require 'commander'
15chalk = require 'chalk'
16semver = require 'semver'
17ckDeps = require 'check-dependencies'
18merge = require 'deepmerge'
19hb = require 'handlebars'
20pkgJson = require __dirname + '/package.json'
21
22nodeVersion = pkgJson.engines.node
23resources = __dirname + '/resources'
24validNameRx = /^[A-Z][0-9A-Z]*$/i
25camelRx = /([a-z])([A-Z])/g
26projNameRx = /\$PROJECT_NAME\$/g
27projNameHyphRx = /\$PROJECT_NAME_HYPHENATED\$/g
28projNameUsRx = /\$PROJECT_NAME_UNDERSCORED\$/g
29interfaceDepsRx = /\$INTERFACE_DEPS\$/g
30platformRx = /\$PLATFORM\$/g
31platformCleanRx = /#_\(\$PLATFORM_CLEAN\$\)/g
32platformCleanId = "#_($PLATFORM_CLEAN$)"
33ipAddressRx = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/i
34debugHostRx = /host]\s+\?:\s+@".*";/g
35namespaceRx = /\(ns\s+([A-Za-z0-9.-]+)/g
36jsRequireRx = /js\/require "(.+)"/g
37rnVersion = '0.48.4'
38rnWinVersion = '0.48.0-rc.4'
39rnPackagerPort = 8081
40process.title = 're-natal'
41buildProfiles =
42 dev:
43 profilesRx: /#_\(\$DEV_PROFILES\$\)/g
44 profilesId: "#_($DEV_PROFILES$)"
45 prod:
46 profilesRx: /#_\(\$PROD_PROFILES\$\)/g
47 profilesId: "#_($PROD_PROFILES$)"
48 advanced:
49 profilesRx: /#_\(\$ADVANCED_PROFILES\$\)/g
50 profilesId: "#_($ADVANCED_PROFILES$)"
51interfaceConf =
52 'reagent':
53 cljsDir: "cljs-reagent"
54 sources:
55 common: ["handlers.cljs", "subs.cljs", "db.cljs"]
56 other: []
57 deps: ['[reagent "0.5.1" :exclusions [cljsjs/react]]'
58 '[re-frame "0.6.0"]']
59 shims: ["cljsjs.react"]
60 sampleCommandNs: '(in-ns \'$PROJECT_NAME_HYPHENATED$.ios.core)'
61 sampleCommand: '(dispatch [:set-greeting "Hello Native World!"])'
62 'reagent6':
63 cljsDir: "cljs-reagent6"
64 sources:
65 common: ["events.cljs", "subs.cljs", "db.cljs"]
66 other: [["reagent_dom.cljs","reagent/dom.cljs"], ["reagent_dom_server.cljs","reagent/dom/server.cljs"]]
67 deps: ['[reagent "0.7.0" :exclusions [cljsjs/react cljsjs/react-dom cljsjs/react-dom-server cljsjs/create-react-class]]'
68 '[re-frame "0.9.2"]']
69 shims: ["cljsjs.react", "cljsjs.react.dom", "cljsjs.react.dom.server", "cljsjs.create-react-class"]
70 sampleCommandNs: '(in-ns \'$PROJECT_NAME_HYPHENATED$.ios.core)'
71 sampleCommand: '(dispatch [:set-greeting "Hello Native World!"])'
72 'om-next':
73 cljsDir: "cljs-om-next"
74 sources:
75 common: ["state.cljs"]
76 other: [["support.cljs","re_natal/support.cljs"]]
77 deps: ['[org.omcljs/om "1.0.0-beta1" :exclusions [cljsjs/react cljsjs/react-dom]]']
78 shims: ["cljsjs.react", "cljsjs.react.dom"]
79 sampleCommandNs: '(in-ns \'$PROJECT_NAME_HYPHENATED$.state)'
80 sampleCommand: '(swap! app-state assoc :app/msg "Hello Native World!")'
81 'rum':
82 cljsDir: "cljs-rum"
83 sources:
84 common: []
85 other: [["sablono_compiler.clj","sablono/compiler.clj"],["support.cljs","re_natal/support.cljs"]]
86 deps: ['[rum "0.10.8" :exclusions [cljsjs/react cljsjs/react-dom sablono]]']
87 shims: ["cljsjs.react", "cljsjs.react.dom", "sablono.core"]
88 sampleCommandNs: '(in-ns \'$PROJECT_NAME_HYPHENATED$.ios.core)'
89 sampleCommand: '(swap! app-state assoc :greeting "Hello Clojure in iOS and Android with Rum!")'
90interfaceNames = Object.keys interfaceConf
91defaultInterface = 'reagent6'
92defaultEnvRoots =
93 dev: 'env/dev'
94 prod: 'env/prod'
95platformMeta =
96 'ios':
97 name: "iOS"
98 sources: ["core.cljs"]
99 'android':
100 name: "Android"
101 sources: ["core.cljs"]
102 'windows':
103 name: "UWP"
104 sources: ["core.cljs"]
105 'wpf':
106 name: "WPF"
107 sources: ["core.cljs"]
108
109log = (s, color = 'green') ->
110 console.log chalk[color] s
111
112
113logErr = (err, color = 'red') ->
114 console.error chalk[color] err
115 process.exit 1
116
117
118exec = (cmd, keepOutput) ->
119 if keepOutput
120 child.execSync cmd, stdio: 'inherit'
121 else
122 child.execSync cmd, stdio: ['pipe', 'pipe', 'ignore']
123
124ensureExecutableAvailable = (executable) ->
125 if os.platform() == 'win32'
126 try
127 exec "where #{executable}"
128 catch e
129 throw new Error("type: #{executable}: not found")
130 else
131 exec "type #{executable}"
132
133isYarnAvailable = () ->
134 try
135 ensureExecutableAvailable('yarn')
136 true
137 catch e
138 false
139
140isSomeDepsMissing = () ->
141 depState = ckDeps.sync {install: false, verbose: false}
142 !depState.depsWereOk
143
144installDeps = (opts = verbose: false, report: false) ->
145 {verbose, report} = opts
146 if report
147 ckDeps.sync (install: false, verbose: true)
148 if isYarnAvailable()
149 exec 'yarn', verbose
150 else
151 exec 'npm i', verbose
152
153ensureOSX = (cb) ->
154 if os.platform() == 'darwin'
155 cb()
156 else
157 logErr 'This command is only available on OSX'
158
159readFile = (path) ->
160 fs.readFileSync path, encoding: 'ascii'
161
162
163edit = (path, pairs) ->
164 fs.writeFileSync path, pairs.reduce (contents, [rx, replacement]) ->
165 contents.replace rx, replacement
166 , readFile path
167
168toUnderscored = (s) ->
169 s.replace(camelRx, '$1_$2').toLowerCase()
170
171checkPort = (port, cb) ->
172 sock = net.connect {port}, ->
173 sock.end()
174 http.get "http://localhost:#{port}/status", (res) ->
175 data = ''
176 res.on 'data', (chunk) -> data += chunk
177 res.on 'end', ->
178 cb data.toString() isnt 'packager-status:running'
179
180 .on 'error', -> cb true
181 .setTimeout 3000
182
183 sock.on 'error', ->
184 sock.end()
185 cb false
186
187ensureFreePort = (cb) ->
188 checkPort rnPackagerPort, (inUse) ->
189 if inUse
190 logErr "
191 Port #{rnPackagerPort} is currently in use by another process
192 and is needed by the React Native packager.
193 "
194 cb()
195
196ensureXcode = (cb) ->
197 try
198 ensureExecutableAvailable 'xcodebuild'
199 cb();
200 catch {message}
201 if message.match /type.+xcodebuild/i
202 logErr 'Xcode Command Line Tools are required'
203
204generateConfig = (interfaceName, platforms, projName) ->
205 log 'Creating Re-Natal config'
206 config =
207 name: projName
208 interface: interfaceName
209 envRoots: defaultEnvRoots
210 modules: []
211 imageDirs: ["images"]
212 platforms: {}
213 autoRequire: false
214
215 for platform in platforms
216 config.platforms[platform] =
217 host: "localhost"
218 modules: []
219
220 writeConfig config
221
222writeConfig = (config, file = ".re-natal") ->
223 try
224 fs.writeFileSync "./#{file}", JSON.stringify config, null, 2
225 config
226 catch {message}
227 logErr message
228 logErr \
229 if message.match /EACCES/i
230 "Invalid write permissions for creating #{file} config file"
231 else
232 message
233
234verifyConfig = (config) ->
235 if !config.platforms? || !config.modules? || !config.imageDirs? || !config.interface? || !config.envRoots?
236 throw new Error 're-natal project needs to be upgraded, please run: re-natal upgrade'
237 config
238
239readConfig = (file = '.re-natal', mustExist = true, defaultValue = {}) ->
240 try
241 if (mustExist || fs.existsSync(file))
242 JSON.parse readFile file
243 else
244 defaultValue
245 catch {message}
246 logErr \
247 if message.match /ENOENT/i
248 "No Re-Natal config was found in this directory (#{file})"
249 else if message.match /EACCES/i
250 "No read permissions for #{file}"
251 else if message.match /Unexpected/i
252 "#{file} contains malformed JSON"
253 else
254 message
255
256readAndVerifyConfig = (file) ->
257 verifyConfig readConfig file
258
259readLocalConfig = () ->
260 global = readConfig '.re-natal'
261 local = readConfig '.re-natal.local', false
262 verifyConfig merge(global, local)
263
264scanImageDir = (dir, platforms) ->
265 fnames = fs.readdirSync(dir)
266 .map (fname) -> "#{dir}/#{fname}"
267 .filter (path) -> fs.statSync(path).isFile()
268 .filter (path) -> removeExcludeFiles(path)
269 .map (path) -> path.replace /@2x|@3x/i, ''
270 .map (path) -> path.replace new RegExp(".(#{platforms.join('|')})" + fpath.extname(path) + "$", "i"), fpath.extname(path)
271 .filter (v, idx, slf) -> slf.indexOf(v) == idx
272
273 dirs = fs.readdirSync(dir)
274 .map (fname) -> "#{dir}/#{fname}"
275 .filter (path) -> fs.statSync(path).isDirectory()
276 fnames.concat scanImages(dirs)
277
278removeExcludeFiles = (file) ->
279 excludedFileNames = [".DS_Store"]
280 res = excludedFileNames.map (ex) -> (file.indexOf ex) == -1
281 true in res
282
283scanImages = (dirs, platforms) ->
284 imgs = []
285 for dir in dirs
286 imgs = imgs.concat(scanImageDir(dir, platforms));
287 imgs
288
289resolveAndroidDevHost = (deviceType) ->
290 allowedTypes = {'real': 'localhost', 'avd': '10.0.2.2', 'genymotion': '10.0.3.2'}
291 devHost = allowedTypes[deviceType]
292 if (devHost?)
293 log "Using '#{devHost}' for device type #{deviceType}"
294 devHost
295 else
296 deviceTypeIsIpAddress(deviceType, Object.keys(allowedTypes))
297
298configureDevHostForAndroidDevice = (deviceType, globally = false) ->
299 try
300 configFile = if globally then '.re-natal' else '.re-natal.local'
301 devHost = resolveAndroidDevHost(deviceType)
302 config = merge(readConfig(configFile, false), platforms: android: host: devHost)
303 writeConfig(config, configFile)
304 log "Please run: re-natal use-figwheel to take effect."
305 catch {message}
306 logErr message
307
308resolveIosDevHost = (deviceType) ->
309 if deviceType == 'simulator'
310 log "Using 'localhost' for iOS simulator"
311 'localhost'
312 else if deviceType == 'real'
313 en0Ip = exec('ipconfig getifaddr en0').toString().trim()
314 log "Using IP of interface en0:'#{en0Ip}' for real iOS device"
315 en0Ip
316 else
317 deviceTypeIsIpAddress(deviceType, ['simulator', 'real'])
318
319configureDevHostForIosDevice = (deviceType, globally = false) ->
320 try
321 configFile = if globally then '.re-natal' else '.re-natal.local'
322 devHost = resolveIosDevHost(deviceType)
323 config = merge(readConfig(configFile, false), platforms: ios: host: devHost)
324 writeConfig(config, configFile)
325 log "Please run: re-natal use-figwheel to take effect."
326 catch {message}
327 logErr message
328
329deviceTypeIsIpAddress = (deviceType, allowedTypes) ->
330 if deviceType.match(ipAddressRx)
331 log "Using development host IP: '#{deviceType}'"
332 deviceType
333 else
334 log("Value '#{deviceType}' is not a valid IP address, still configured it as development host. Did you mean one of: [#{allowedTypes}] ?", 'yellow')
335 deviceType
336
337copyDevEnvironmentFilesForPlatform = (platform, interfaceName, projNameHyph, projName, devEnvRoot) ->
338 cljsDir = interfaceConf[interfaceName].cljsDir
339 fs.mkdirpSync "#{devEnvRoot}/env/#{platform}"
340 mainDevPath = "#{devEnvRoot}/env/#{platform}/main.cljs"
341 fs.copySync("#{resources}/#{cljsDir}/main_dev.cljs", mainDevPath)
342 edit mainDevPath, [[projNameHyphRx, projNameHyph], [projNameRx, projName], [platformRx, platform]]
343
344generateConfigNs = (config) ->
345 template = hb.compile(readFile "#{resources}/config.cljs")
346 fs.writeFileSync("#{config.envRoots.dev}/env/config.cljs", template(config))
347
348copyDevEnvironmentFiles = (interfaceName, platforms, projNameHyph, projName, devEnvRoot) ->
349 userNsPath = "#{devEnvRoot}/user.clj"
350 fs.copySync("#{resources}/user.clj", userNsPath)
351
352 for platform in platforms
353 copyDevEnvironmentFilesForPlatform platform, interfaceName, projNameHyph, projName, devEnvRoot
354
355copyProdEnvironmentFilesForPlatform = (platform, interfaceName, projNameHyph, projName, prodEnvRoot) ->
356 cljsDir = interfaceConf[interfaceName].cljsDir
357 fs.mkdirpSync "#{prodEnvRoot}/env/#{platform}"
358 mainProdPath = "#{prodEnvRoot}/env/#{platform}/main.cljs"
359 fs.copySync("#{resources}/#{cljsDir}/main_prod.cljs", mainProdPath)
360 edit mainProdPath, [[projNameHyphRx, projNameHyph], [projNameRx, projName], [platformRx, platform]]
361
362copyProdEnvironmentFiles = (interfaceName, platforms, projNameHyph, projName, prodEnvRoot) ->
363 for platform in platforms
364 copyProdEnvironmentFilesForPlatform platform, interfaceName, projNameHyph, projName, prodEnvRoot
365
366copyFigwheelBridge = (projNameUs) ->
367 fs.copySync("#{resources}/figwheel-bridge.js", "./figwheel-bridge.js")
368 edit "figwheel-bridge.js", [[projNameUsRx, projNameUs]]
369
370updateGitIgnore = (platforms) ->
371 fs.appendFileSync(".gitignore", "\n# Generated by re-natal\n#\n")
372
373 indexFiles = platforms.map (platform) -> "index.#{platform}.js"
374 fs.appendFileSync(".gitignore", indexFiles.join("\n"))
375 fs.appendFileSync(".gitignore", "\ntarget/")
376 fs.appendFileSync(".gitignore", "\n.re-natal.local")
377 fs.appendFileSync(".gitignore", "\nenv/dev/env/config.cljs\n")
378
379 fs.appendFileSync(".gitignore", "\n# Figwheel\n#\nfigwheel_server.log")
380
381findPackagerFileToPatch = () ->
382 files = [
383 "node_modules/metro-bundler/src/Server/index.js",
384 "node_modules/metro-bundler/build/Server/index.js",
385 "node_modules/react-native/packager/src/Server/index.js"]
386 fileToPatch = files[0];
387 for f in files
388 if fs.existsSync(f)
389 fileToPatch = f
390 fileToPatch
391
392patchReactNativePackager = () ->
393 installDeps()
394 fileToPatch = findPackagerFileToPatch()
395 log "Patching file #{fileToPatch} to serve *.map files."
396 edit fileToPatch,
397 [[/match.*\.map\$\/\)/m, "match(/index\\..*\\.map$/)"]]
398 log "If the React Native packager is running, please restart it."
399
400shimCljsNamespace = (ns) ->
401 filePath = "src/" + ns.replace(/\./g, "/") + ".cljs"
402 filePath = filePath.replace(/-/g, "_")
403 fs.mkdirpSync fpath.dirname(filePath)
404 fs.writeFileSync(filePath, "(ns #{ns})")
405
406copySrcFilesForPlatform = (platform, interfaceName, projName, projNameUs, projNameHyph) ->
407 cljsDir = interfaceConf[interfaceName].cljsDir
408 fs.mkdirSync "src/#{projNameUs}/#{platform}"
409 fileNames = platformMeta[platform].sources
410 for fileName in fileNames
411 path = "src/#{projNameUs}/#{platform}/#{fileName}"
412 fs.copySync("#{resources}/#{cljsDir}/#{fileName}", path)
413 edit path, [[projNameHyphRx, projNameHyph], [projNameRx, projName], [platformRx, platform]]
414
415copySrcFiles = (interfaceName, platforms, projName, projNameUs, projNameHyph) ->
416 cljsDir = interfaceConf[interfaceName].cljsDir
417
418 fileNames = interfaceConf[interfaceName].sources.common;
419 for fileName in fileNames
420 path = "src/#{projNameUs}/#{fileName}"
421 fs.copySync("#{resources}/#{cljsDir}/#{fileName}", path)
422 edit path, [[projNameHyphRx, projNameHyph], [projNameRx, projName]]
423
424 for platform in platforms
425 copySrcFilesForPlatform platform, interfaceName, projName, projNameUs, projNameHyph
426
427 otherFiles = interfaceConf[interfaceName].sources.other;
428 for cpFile in otherFiles
429 from = "#{resources}/#{cljsDir}/#{cpFile[0]}"
430 to = "src/#{cpFile[1]}"
431 fs.copySync(from, to)
432
433 shims = fileNames = interfaceConf[interfaceName].shims;
434 for namespace in shims
435 shimCljsNamespace(namespace)
436
437creteBuildConfigs = (profiles, platforms) ->
438 builds = {}
439 for profile in profiles
440 template = readFile "#{resources}/#{profile}.profile"
441 configs = platforms.map (platform) -> template.replace(platformRx, platform)
442 configs.push buildProfiles[profile].profilesId
443 builds[profile] = configs.join("\n")
444 builds
445
446copyProjectClj = (interfaceName, platforms, projNameHyph) ->
447 fs.copySync("#{resources}/project.clj", "project.clj")
448 deps = interfaceConf[interfaceName].deps.join("\n")
449
450 cleans = platforms.map (platform) -> "\"index.#{platform}.js\""
451 cleans.push platformCleanId
452
453 builds = creteBuildConfigs ['dev', 'prod', 'advanced'], platforms
454
455 edit 'project.clj', [
456 [projNameHyphRx, projNameHyph],
457 [interfaceDepsRx, deps],
458 [platformCleanRx, cleans.join(' ')],
459 [buildProfiles.dev.profilesRx, builds.dev],
460 [buildProfiles.prod.profilesRx, builds.prod],
461 [buildProfiles.advanced.profilesRx, builds.advanced]]
462
463updateProjectClj = (platform) ->
464 proj = readFile('project.clj')
465
466 cleans = []
467 cleans.push "\"index.#{platform}.js\""
468 cleans.push platformCleanId
469
470 if !proj.match(platformCleanRx)
471 log "Manual update of project.clj required: add clean targets:"
472 log "#{cleans.join(' ')}", "red"
473
474 builds = creteBuildConfigs ['dev', 'prod', 'advanced'], [platform]
475
476 profileKeys = Object.keys buildProfiles
477 for key in profileKeys
478 if !proj.match(buildProfiles[key].profilesRx)
479 log "Manual update of project.clj required: add new build to #{key} profile:"
480 log "#{builds[key]}", "red"
481
482 edit 'project.clj', [
483 [platformCleanRx, cleans.join(' ')],
484 [buildProfiles.dev.profilesRx, builds.dev],
485 [buildProfiles.prod.profilesRx, builds.prod],
486 [buildProfiles.advanced.profilesRx, builds.advanced]
487 ]
488
489init = (interfaceName, projName, platforms) ->
490 if projName.toLowerCase() is 'react' or !projName.match validNameRx
491 logErr 'Invalid project name. Use an alphanumeric CamelCase name.'
492
493 projNameHyph = projName.replace(camelRx, '$1-$2').toLowerCase()
494 projNameUs = toUnderscored projName
495
496 try
497 log "Creating #{projName}", 'bgMagenta'
498 if isYarnAvailable()
499 log '\u2615 Grab a coffee! I will use yarn, but fetching deps still takes time...', 'yellow'
500 else
501 log '\u2615 Grab a coffee! Downloading deps might take a while...', 'yellow'
502
503 if fs.existsSync projNameHyph
504 throw new Error "Directory #{projNameHyph} already exists"
505
506 ensureExecutableAvailable 'lein'
507
508 log 'Creating Leiningen project'
509 exec "lein new #{projNameHyph}"
510
511 log 'Updating Leiningen project'
512 process.chdir projNameHyph
513 fs.removeSync "resources"
514 corePath = "src/#{projNameUs}/core.clj"
515 fs.unlinkSync corePath
516
517 copyProjectClj(interfaceName, platforms, projNameHyph)
518
519 copySrcFiles(interfaceName, platforms, projName, projNameUs, projNameHyph)
520
521 copyDevEnvironmentFiles(interfaceName, platforms, projNameHyph, projName, defaultEnvRoots.dev)
522 copyProdEnvironmentFiles(interfaceName, platforms, projNameHyph, projName, defaultEnvRoots.prod)
523
524 fs.copySync("#{resources}/images", "./images")
525
526 log 'Creating React Native skeleton.'
527
528 pkg =
529 name: projName
530 version: '0.0.1'
531 private: true
532 scripts:
533 start: 'node node_modules/react-native/local-cli/cli.js start'
534 dependencies:
535 'react-native': rnVersion
536 # Fixes issue with packager 'TimeoutError: transforming ... took longer than 301 seconds.'
537 'babel-plugin-transform-es2015-block-scoping': '6.15.0'
538
539 if 'windows' in platforms || 'wpf' in platforms
540 pkg.dependencies['react-native-windows'] = rnWinVersion
541
542 fs.writeFileSync 'package.json', JSON.stringify pkg, null, 2
543
544 installDeps()
545
546 fs.unlinkSync '.gitignore'
547 exec "node -e
548 \"require('react-native/local-cli/cli').init('.', '#{projName}')\"
549 "
550
551 if 'windows' in platforms
552 log 'Creating React Native UWP project.'
553 exec "node -e
554 \"require('react-native-windows/local-cli/generate-windows')('.', '#{projName}', '#{projName}')\"
555 "
556
557 if 'wpf' in platforms
558 log 'Creating React Native WPF project.'
559 exec "node -e
560 \"require('react-native-windows/local-cli/generate-wpf')('.', '#{projName}', '#{projName}')\"
561 "
562
563 updateGitIgnore(platforms)
564
565 config = generateConfig(interfaceName, platforms, projName)
566 generateConfigNs(config);
567
568 copyFigwheelBridge(projNameUs)
569
570 log 'Compiling ClojureScript'
571 exec 'lein prod-build'
572
573 log ''
574 log 'To get started with your new app, first cd into its directory:', 'yellow'
575 log "cd #{projNameHyph}", 'inverse'
576 log ''
577 log 'Run iOS app:' , 'yellow'
578 log 'react-native run-ios > /dev/null', 'inverse'
579 log ''
580 log 'To use figwheel type:' , 'yellow'
581 log 're-natal use-figwheel', 'inverse'
582 log 'lein figwheel ios', 'inverse'
583 log ''
584 log 'Reload the app in simulator (\u2318 + R)'
585 log ''
586 log 'At the REPL prompt type this:', 'yellow'
587 log interfaceConf[interfaceName].sampleCommandNs.replace(projNameHyphRx, projNameHyph), 'inverse'
588 log ''
589 log 'Changes you make via the REPL or by changing your .cljs files should appear live.', 'yellow'
590 log ''
591 log 'Try this command as an example:', 'yellow'
592 log interfaceConf[interfaceName].sampleCommand, 'inverse'
593 log ''
594 log '✔ Done', 'bgMagenta'
595 log ''
596
597 catch {message}
598 logErr \
599 if message.match /type.+lein/i
600 'Leiningen is required (http://leiningen.org)'
601 else if message.match /npm/i
602 "npm install failed. This may be a network issue. Check #{projNameHyph}/npm-debug.log for details."
603 else
604 message
605
606addPlatform = (platform) ->
607 try
608 if !(platform of platformMeta)
609 throw new Error "Unknown platform [#{platform}]"
610
611 config = readAndVerifyConfig()
612 platforms = Object.keys config.platforms
613
614 if platform in platforms
615 throw new Error "A project for a #{platformMeta[platform].name} app already exists"
616 else
617 interfaceName = config.interface
618 projName = config.name
619 projNameHyph = projName.replace(camelRx, '$1-$2').toLowerCase()
620 projNameUs = toUnderscored projName
621
622 log "Preparing for #{platformMeta[platform].name} app."
623
624 updateProjectClj(platform)
625 copySrcFilesForPlatform(platform, interfaceName, projName, projNameUs, projNameHyph)
626 copyDevEnvironmentFilesForPlatform(platform, interfaceName, projNameHyph, projName, defaultEnvRoots.dev)
627 copyProdEnvironmentFilesForPlatform(platform, interfaceName, projNameHyph, projName, defaultEnvRoots.prod)
628
629 pkg = JSON.parse readFile 'package.json'
630
631 unless 'react-native-windows' in pkg.dependencies
632 pkg.dependencies['react-native-windows'] = rnWinVersion
633 fs.writeFileSync 'package.json', JSON.stringify pkg, null, 2
634 installDeps()
635
636 if platform is 'windows'
637 log 'Creating React Native UWP project.'
638 exec "node -e
639 \"require('react-native-windows/local-cli/generate-windows')('.', '#{projName}', '#{projName}')\"
640 "
641
642 if platform is 'wpf'
643 log 'Creating React Native WPF project.'
644 exec "node -e
645 \"require('react-native-windows/local-cli/generate-wpf')('.', '#{projName}', '#{projName}')\"
646 "
647
648 fs.appendFileSync(".gitignore", "\n\nindex.#{platform}.js\n")
649
650 config.platforms[platform] =
651 host: "localhost"
652 modules: []
653 generateConfigNs(config)
654
655 writeConfig(config)
656
657 log 'Compiling ClojureScript'
658 exec 'lein prod-build'
659 catch {message}
660 logErr message
661
662openXcode = (name) ->
663 try
664 exec "open ios/#{name}.xcodeproj"
665 catch {message}
666 logErr \
667 if message.match /ENOENT/i
668 """
669 Cannot find #{name}.xcodeproj in ios.
670 Run this command from your project's root directory.
671 """
672 else if message.match /EACCES/i
673 "Invalid permissions for opening #{name}.xcodeproj in ios"
674 else
675 message
676
677generateRequireModulesCode = (modules) ->
678 jsCode = "var modules={'react-native': require('react-native'), 'react': require('react'), 'create-react-class': require('create-react-class')};"
679 for m in modules
680 jsCode += "modules['#{m}']=require('#{m}');";
681 jsCode += '\n'
682
683updateIosRCTWebSocketExecutor = (iosHost) ->
684 RCTWebSocketExecutorPath = "node_modules/react-native/Libraries/WebSocket/RCTWebSocketExecutor.m"
685 edit RCTWebSocketExecutorPath, [[debugHostRx, "host] ?: @\"#{iosHost}\";"]]
686
687platformOfNamespace = (ns) ->
688 if ns?
689 possiblePlatforms = Object.keys platformMeta
690 p = possiblePlatforms.find((p) -> ns.indexOf(".#{p}") > 0);
691 p ?= "common"
692
693extractRequiresFromSourceFile = (file) ->
694 content = fs.readFileSync(file, encoding: 'utf8')
695 requires = []
696 while match = namespaceRx.exec(content)
697 ns = match[1]
698 while match = jsRequireRx.exec(content)
699 requires.push(match[1])
700
701 platform: platformOfNamespace(ns)
702 requires: requires
703
704buildRequireByPlatformMap = () ->
705 onlyUserCljs = (item) -> fpath.extname(item.path) == '.cljs' and
706 item.path.indexOf('/target/') < 0 # ignore target dir
707 files = klawSync process.cwd(),
708 nodir: true
709 filter: onlyUserCljs
710 filenames = files.map((o) -> o.path)
711 extractedRequires = filenames.map(extractRequiresFromSourceFile)
712
713 extractedRequires.reduce((result, item) ->
714 platform = item.platform
715 if result[platform]?
716 result[platform] = Array.from(new Set(item.requires.concat(result[platform])))
717 else
718 result[platform] = Array.from(new Set(item.requires))
719 result
720 , {})
721
722platformModulesAndImages = (config, platform) ->
723 if config.autoRequire? and config.autoRequire
724 requires = buildRequireByPlatformMap()
725 requires.common.concat(requires[platform])
726 else
727 platforms = Object.keys config.platforms
728 images = scanImages(config.imageDirs, platforms).map (fname) -> './' + fname;
729 modulesAndImages = config.modules.concat images;
730 if typeof config.platforms[platform].modules is 'undefined'
731 modulesAndImages
732 else
733 modulesAndImages.concat(config.platforms[platform].modules)
734
735generateDevScripts = () ->
736 try
737 config = readLocalConfig()
738 platforms = Object.keys config.platforms
739 projName = config.name
740
741 if isSomeDepsMissing()
742 installDeps(verbose: true)
743
744 log 'Cleaning...'
745 exec 'lein clean'
746
747 devHost = {}
748 for platform in platforms
749 devHost[platform] = config.platforms[platform].host
750
751 if config.autoRequire? and config.autoRequire
752 log 'Auto-require is enabled. Scanning for require() calls in *.cljs files...'
753
754 for platform in platforms
755 moduleMap = generateRequireModulesCode(platformModulesAndImages(config, platform))
756 fs.writeFileSync "index.#{platform}.js", "#{moduleMap}require('figwheel-bridge').withModules(modules).start('#{projName}','#{platform}','#{devHost[platform]}');"
757 log "index.#{platform}.js was regenerated"
758
759 updateIosRCTWebSocketExecutor(devHost.ios)
760 log "Host in RCTWebSocketExecutor.m was updated"
761
762 generateConfigNs(config);
763 for platform in platforms
764 log "Dev server host for #{platformMeta[platform].name}: #{devHost[platform]}"
765
766 catch {message}
767 logErr \
768 if message.match /EACCES/i
769 'Invalid write permissions for creating development scripts'
770 else
771 message
772
773doUpgrade = (config) ->
774 projName = config.name
775 projNameHyph = projName.replace(camelRx, '$1-$2').toLowerCase()
776 projNameUs = toUnderscored projName
777 platforms = Object.keys config.platforms
778
779 unless config.interface
780 config.interface = defaultInterface
781
782 unless config.modules
783 config.modules = []
784
785 unless config.imageDirs
786 config.imageDirs = ["images"]
787
788 unless config.envRoots
789 config.envRoots = defaultEnvRoots
790
791 unless config.platforms
792 config.platforms =
793 ios:
794 host: "localhost"
795 modules: []
796 android:
797 host: "localhost"
798 modules: []
799
800 if config.iosHost?
801 config.platforms.ios.host = config.iosHost
802 delete config.iosHost
803
804 if config.androidHost?
805 config.platforms.android.host = config.androidHost
806 delete config.androidHost
807
808 if config.modulesPlatform?
809 if config.modulesPlatform.ios?
810 config.platforms.ios.modules = config.platforms.ios.modules.concat(config.modulesPlatform.ios)
811
812 if config.modulesPlatform.android?
813 config.platforms.android.modules = config.platforms.android.modules.concat(config.modulesPlatform.android)
814
815 delete config.modulesPlatform
816
817 writeConfig(config)
818 log 'upgraded .re-natal'
819
820 interfaceName = config.interface
821 envRoots = config.envRoots
822
823 copyDevEnvironmentFiles(interfaceName, platforms, projNameHyph, projName, envRoots.dev)
824 copyProdEnvironmentFiles(interfaceName, platforms, projNameHyph, projName, envRoots.prod)
825 generateConfigNs(config);
826 log "upgraded files in #{envRoots.dev} and #{envRoots.prod} "
827
828 copyFigwheelBridge(projNameUs)
829 log 'upgraded figwheel-bridge.js'
830 log('To upgrade React Native version please follow the official guide in https://facebook.github.io/react-native/docs/upgrading.html', 'yellow')
831
832useComponent = (name, platform) ->
833 try
834 config = readAndVerifyConfig()
835 platforms = Object.keys config.platforms
836 if typeof platform isnt 'string'
837 config.modules.push name
838 log "Component '#{name}' is now configured for figwheel, please re-run 'use-figwheel' command to take effect"
839 else if platforms.indexOf(platform) > -1
840 if typeof config.platforms[platform].modules is 'undefined'
841 config.platforms[platform].modules = []
842 config.platforms[platform].modules.push name
843 log "Component '#{name}' (#{platform}-only) is now configured for figwheel, please re-run 'use-figwheel' command to take effect"
844 else
845 throw new Error("unsupported platform: #{platform}")
846 writeConfig(config)
847 catch {message}
848 logErr message
849
850logModuleDifferences = (platform, existingModules, newModules) ->
851 existingModuleSet = new Set(existingModules)
852 newModuleSet = new Set(newModules)
853
854 addedModules = new Set(newModules.filter((m) -> !existingModuleSet.has(m)))
855 removedModules = new Set(existingModules.filter((m) -> !newModuleSet.has(m)))
856
857 if(removedModules.size isnt 0)
858 log "removed #{platform} modules #{Array.from(removedModules)}"
859 if(addedModules.size isnt 0)
860 log "new #{platform} modules found #{Array.from(addedModules)}"
861
862
863inferComponents = () ->
864 requiresByPlatform = buildRequireByPlatformMap()
865
866 config = readAndVerifyConfig() # re-natal file
867 logModuleDifferences('common', config.modules, requiresByPlatform.common)
868 config.modules = requiresByPlatform.common
869
870 platforms = Object.keys config.platforms
871 for platform in platforms
872 logModuleDifferences(platform, config.platforms[platform].modules, requiresByPlatform[platform])
873 config.platforms[platform].modules = requiresByPlatform[platform]
874
875 writeConfig(config)
876
877autoRequire = (enabled, globally = false) ->
878 configFile = if globally then '.re-natal' else '.re-natal.local'
879 config = merge(readConfig(configFile, false), autoRequire: enabled)
880 writeConfig(config, configFile)
881 if (enabled)
882 log "Auto-Require feature is enabled in use-figwheel command"
883 else
884 log "Auto-Require feature is disabled in use-figwheel command"
885
886cli._name = 're-natal'
887cli.version pkgJson.version
888
889cli.command 'init <name>'
890 .description 'create a new ClojureScript React Native project'
891 .option "-i, --interface [#{interfaceNames.join ' '}]", 'specify React interface', defaultInterface
892 .option '-u, --uwp', 'create project for UWP app'
893 .option '-w, --wpf', 'create project for WPF app'
894 .action (name, cmd) ->
895 if typeof name isnt 'string'
896 logErr '''
897 re-natal init requires a project name as the first argument.
898 e.g.
899 re-natal init HelloWorld
900 '''
901 unless interfaceConf[cmd.interface]
902 logErr "Unsupported React interface: #{cmd.interface}, one of [#{interfaceNames}] was expected."
903 platforms = ['ios', 'android']
904 if cmd.uwp?
905 platforms.push 'windows'
906 if cmd.wpf?
907 platforms.push 'wpf'
908 ensureFreePort -> init(cmd.interface, name, platforms)
909
910cli.command 'upgrade'
911.description 'upgrades project files to current installed version of re-natal (the upgrade of re-natal itself is done via npm)'
912.action ->
913 doUpgrade readConfig()
914
915cli.command 'add-platform <platform>'
916 .description 'adds additional app platform: \'windows\' - UWP app, \'wpf\' - WPF app'
917 .action (platform) ->
918 addPlatform(platform)
919
920cli.command 'xcode'
921 .description 'open Xcode project'
922 .action ->
923 ensureOSX ->
924 ensureXcode ->
925 openXcode readAndVerifyConfig().name
926
927cli.command 'deps'
928 .description 'install all dependencies for the project'
929 .action ->
930 installDeps(verbose: true, report: true)
931
932cli.command 'use-figwheel'
933 .description 'generate index.*.js for development with figwheel'
934 .action () ->
935 generateDevScripts()
936
937cli.command 'use-android-device <type>'
938 .description 'sets up the host for android device type: \'real\' - localhost, \'avd\' - 10.0.2.2, \'genymotion\' - 10.0.3.2, IP'
939 .option '-g --global', 'use global .re-natal config instead of .re-natal.local'
940 .action (type, cmd) ->
941 configureDevHostForAndroidDevice type, cmd.global
942
943cli.command 'use-ios-device <type>'
944 .description 'sets up the host for ios device type: \'simulator\' - localhost, \'real\' - auto detect IP on eth0, IP'
945 .option '-g --global', 'use global .re-natal config instead of .re-natal.local'
946 .action (type, cmd) ->
947 configureDevHostForIosDevice type, cmd.global
948
949cli.command 'use-component <name> [<platform>]'
950 .description 'configures a custom component to work with figwheel. Same as \'require\' command.'
951 .action (name, platform) ->
952 useComponent(name, platform)
953
954cli.command 'require <name> [<platform>]'
955 .description 'configures an external module to work with figwheel. name is the value you pass to (js/require) function.'
956 .action (name, platform) ->
957 useComponent(name, platform)
958
959cli.command 'infer-components'
960 .description 'parses all cljs files in this project, extracts all (js/require) calls and adds required modules to .re-natal file'
961 .action () ->
962 inferComponents()
963
964cli.command 'require-all'
965 .description 'parses all cljs files in this project, extracts all (js/require) calls and adds required modules to .re-natal file'
966 .action () ->
967 inferComponents()
968
969cli.command 'enable-source-maps'
970.description 'patches RN packager to server *.map files from filesystem, so that chrome can download them.'
971.action () ->
972 patchReactNativePackager()
973
974cli.command 'enable-auto-require'
975 .description 'enables source scanning for automatic required module resolution in use-figwheel command.'
976 .option '-g --global', 'use global .re-natal config instead of .re-natal.local'
977 .action (cmd) ->
978 autoRequire(true, cmd.global)
979
980cli.command 'disable-auto-require'
981 .description 'disables auto-require feature in use-figwheel command'
982 .option '-g --global', 'use global .re-natal config instead of .re-natal.local'
983 .action (cmd) ->
984 autoRequire(false, cmd.global)
985
986cli.command 'copy-figwheel-bridge'
987 .description 'copy figwheel-bridge.js into project'
988 .action () ->
989 copyFigwheelBridge(readConfig().name)
990 log "Copied figwheel-bridge.js"
991
992cli.on '*', (command) ->
993 logErr "unknown command #{command[0]}. See re-natal --help for valid commands"
994
995
996unless semver.satisfies process.version[1...], nodeVersion
997 logErr """
998 Re-Natal requires Node.js version #{nodeVersion}
999 You have #{process.version[1...]}
1000 """
1001
1002if process.argv.length <= 2
1003 cli.outputHelp()
1004else
1005 cli.parse process.argv