1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | fs = require 'fs-extra'
|
8 | klawSync = require 'klaw-sync'
|
9 | fpath = require 'path'
|
10 | net = require 'net'
|
11 | http = require 'http'
|
12 | os = require 'os'
|
13 | child = require 'child_process'
|
14 | cli = require 'commander'
|
15 | chalk = require 'chalk'
|
16 | semver = require 'semver'
|
17 | ckDeps = require 'check-dependencies'
|
18 | merge = require 'deepmerge'
|
19 | hb = require 'handlebars'
|
20 | pkgJson = require __dirname + '/package.json'
|
21 |
|
22 | nodeVersion = pkgJson.engines.node
|
23 | resources = __dirname + '/resources'
|
24 | validNameRx = /^[A-Z][0-9A-Z]*$/i
|
25 | camelRx = /([a-z])([A-Z])/g
|
26 | projNameRx = /\$PROJECT_NAME\$/g
|
27 | projNameHyphRx = /\$PROJECT_NAME_HYPHENATED\$/g
|
28 | projNameUsRx = /\$PROJECT_NAME_UNDERSCORED\$/g
|
29 | interfaceDepsRx = /\$INTERFACE_DEPS\$/g
|
30 | platformRx = /\$PLATFORM\$/g
|
31 | platformCleanRx = /#_\(\$PLATFORM_CLEAN\$\)/g
|
32 | platformCleanId = "#_($PLATFORM_CLEAN$)"
|
33 | ipAddressRx = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/i
|
34 | debugHostRx = /host]\s+\?:\s+@".*";/g
|
35 | namespaceRx = /\(ns\s+([A-Za-z0-9.-]+)/g
|
36 | jsRequireRx = /js\/require "(.+)"/g
|
37 | rnVersion = '0.48.4'
|
38 | rnWinVersion = '0.48.0-rc.4'
|
39 | rnPackagerPort = 8081
|
40 | process.title = 're-natal'
|
41 | buildProfiles =
|
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$)"
|
51 | interfaceConf =
|
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!")'
|
90 | interfaceNames = Object.keys interfaceConf
|
91 | defaultInterface = 'reagent6'
|
92 | defaultEnvRoots =
|
93 | dev: 'env/dev'
|
94 | prod: 'env/prod'
|
95 | platformMeta =
|
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 |
|
109 | log = (s, color = 'green') ->
|
110 | console.log chalk[color] s
|
111 |
|
112 |
|
113 | logErr = (err, color = 'red') ->
|
114 | console.error chalk[color] err
|
115 | process.exit 1
|
116 |
|
117 |
|
118 | exec = (cmd, keepOutput) ->
|
119 | if keepOutput
|
120 | child.execSync cmd, stdio: 'inherit'
|
121 | else
|
122 | child.execSync cmd, stdio: ['pipe', 'pipe', 'ignore']
|
123 |
|
124 | ensureExecutableAvailable = (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 |
|
133 | isYarnAvailable = () ->
|
134 | try
|
135 | ensureExecutableAvailable('yarn')
|
136 | true
|
137 | catch e
|
138 | false
|
139 |
|
140 | isSomeDepsMissing = () ->
|
141 | depState = ckDeps.sync {install: false, verbose: false}
|
142 | !depState.depsWereOk
|
143 |
|
144 | installDeps = (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 |
|
153 | ensureOSX = (cb) ->
|
154 | if os.platform() == 'darwin'
|
155 | cb()
|
156 | else
|
157 | logErr 'This command is only available on OSX'
|
158 |
|
159 | readFile = (path) ->
|
160 | fs.readFileSync path, encoding: 'ascii'
|
161 |
|
162 |
|
163 | edit = (path, pairs) ->
|
164 | fs.writeFileSync path, pairs.reduce (contents, [rx, replacement]) ->
|
165 | contents.replace rx, replacement
|
166 | , readFile path
|
167 |
|
168 | toUnderscored = (s) ->
|
169 | s.replace(camelRx, '$1_$2').toLowerCase()
|
170 |
|
171 | checkPort = (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 |
|
187 | ensureFreePort = (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 |
|
196 | ensureXcode = (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 |
|
204 | generateConfig = (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 |
|
222 | writeConfig = (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 |
|
234 | verifyConfig = (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 |
|
239 | readConfig = (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 |
|
256 | readAndVerifyConfig = (file) ->
|
257 | verifyConfig readConfig file
|
258 |
|
259 | readLocalConfig = () ->
|
260 | global = readConfig '.re-natal'
|
261 | local = readConfig '.re-natal.local', false
|
262 | verifyConfig merge(global, local)
|
263 |
|
264 | scanImageDir = (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 |
|
278 | removeExcludeFiles = (file) ->
|
279 | excludedFileNames = [".DS_Store"]
|
280 | res = excludedFileNames.map (ex) -> (file.indexOf ex) == -1
|
281 | true in res
|
282 |
|
283 | scanImages = (dirs, platforms) ->
|
284 | imgs = []
|
285 | for dir in dirs
|
286 | imgs = imgs.concat(scanImageDir(dir, platforms));
|
287 | imgs
|
288 |
|
289 | resolveAndroidDevHost = (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 |
|
298 | configureDevHostForAndroidDevice = (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 |
|
308 | resolveIosDevHost = (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 |
|
319 | configureDevHostForIosDevice = (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 |
|
329 | deviceTypeIsIpAddress = (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 |
|
337 | copyDevEnvironmentFilesForPlatform = (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 |
|
344 | generateConfigNs = (config) ->
|
345 | template = hb.compile(readFile "#{resources}/config.cljs")
|
346 | fs.writeFileSync("#{config.envRoots.dev}/env/config.cljs", template(config))
|
347 |
|
348 | copyDevEnvironmentFiles = (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 |
|
355 | copyProdEnvironmentFilesForPlatform = (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 |
|
362 | copyProdEnvironmentFiles = (interfaceName, platforms, projNameHyph, projName, prodEnvRoot) ->
|
363 | for platform in platforms
|
364 | copyProdEnvironmentFilesForPlatform platform, interfaceName, projNameHyph, projName, prodEnvRoot
|
365 |
|
366 | copyFigwheelBridge = (projNameUs) ->
|
367 | fs.copySync("#{resources}/figwheel-bridge.js", "./figwheel-bridge.js")
|
368 | edit "figwheel-bridge.js", [[projNameUsRx, projNameUs]]
|
369 |
|
370 | updateGitIgnore = (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 |
|
381 | findPackagerFileToPatch = () ->
|
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 |
|
392 | patchReactNativePackager = () ->
|
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 |
|
400 | shimCljsNamespace = (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 |
|
406 | copySrcFilesForPlatform = (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 |
|
415 | copySrcFiles = (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 |
|
437 | creteBuildConfigs = (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 |
|
446 | copyProjectClj = (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 |
|
463 | updateProjectClj = (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 |
|
489 | init = (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 |
|
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 |
|
606 | addPlatform = (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 |
|
662 | openXcode = (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 |
|
677 | generateRequireModulesCode = (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 |
|
683 | updateIosRCTWebSocketExecutor = (iosHost) ->
|
684 | RCTWebSocketExecutorPath = "node_modules/react-native/Libraries/WebSocket/RCTWebSocketExecutor.m"
|
685 | edit RCTWebSocketExecutorPath, [[debugHostRx, "host] ?: @\"#{iosHost}\";"]]
|
686 |
|
687 | platformOfNamespace = (ns) ->
|
688 | if ns?
|
689 | possiblePlatforms = Object.keys platformMeta
|
690 | p = possiblePlatforms.find((p) -> ns.indexOf(".#{p}") > 0);
|
691 | p ?= "common"
|
692 |
|
693 | extractRequiresFromSourceFile = (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 |
|
704 | buildRequireByPlatformMap = () ->
|
705 | onlyUserCljs = (item) -> fpath.extname(item.path) == '.cljs' and
|
706 | item.path.indexOf('/target/') < 0
|
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 |
|
722 | platformModulesAndImages = (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 |
|
735 | generateDevScripts = () ->
|
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 |
|
773 | doUpgrade = (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 |
|
832 | useComponent = (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 |
|
850 | logModuleDifferences = (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 |
|
863 | inferComponents = () ->
|
864 | requiresByPlatform = buildRequireByPlatformMap()
|
865 |
|
866 | config = readAndVerifyConfig()
|
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 |
|
877 | autoRequire = (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 |
|
886 | cli._name = 're-natal'
|
887 | cli.version pkgJson.version
|
888 |
|
889 | cli.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 |
|
910 | cli.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 |
|
915 | cli.command 'add-platform <platform>'
|
916 | .description 'adds additional app platform: \'windows\' - UWP app, \'wpf\' - WPF app'
|
917 | .action (platform) ->
|
918 | addPlatform(platform)
|
919 |
|
920 | cli.command 'xcode'
|
921 | .description 'open Xcode project'
|
922 | .action ->
|
923 | ensureOSX ->
|
924 | ensureXcode ->
|
925 | openXcode readAndVerifyConfig().name
|
926 |
|
927 | cli.command 'deps'
|
928 | .description 'install all dependencies for the project'
|
929 | .action ->
|
930 | installDeps(verbose: true, report: true)
|
931 |
|
932 | cli.command 'use-figwheel'
|
933 | .description 'generate index.*.js for development with figwheel'
|
934 | .action () ->
|
935 | generateDevScripts()
|
936 |
|
937 | cli.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 |
|
943 | cli.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 |
|
949 | cli.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 |
|
954 | cli.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 |
|
959 | cli.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 |
|
964 | cli.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 |
|
969 | cli.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 |
|
974 | cli.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 |
|
980 | cli.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 |
|
986 | cli.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 |
|
992 | cli.on '*', (command) ->
|
993 | logErr "unknown command #{command[0]}. See re-natal --help for valid commands"
|
994 |
|
995 |
|
996 | unless semver.satisfies process.version[1...], nodeVersion
|
997 | logErr """
|
998 | Re-Natal requires Node.js version #{nodeVersion}
|
999 | You have #{process.version[1...]}
|
1000 | """
|
1001 |
|
1002 | if process.argv.length <= 2
|
1003 | cli.outputHelp()
|
1004 | else
|
1005 | cli.parse process.argv
|