-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmain.js
437 lines (360 loc) · 12.6 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
// get the package info:
const pkg = require('./package.json')
// require this before any other modules:
require('please-upgrade-node')(pkg)
const prompts = require('prompts')
const path = require('path')
const exit = require('exit')
// used in quick workaround for
// https://github.com/terkelg/prompts/issues/252
const ansiEscapes = require('ansi-escapes')
const { log } = require('console')
const bulb = require('emoji-bulb')
const createReactNativeLibraryModule = require('create-react-native-module')
const exampleTemplates = require('create-react-native-module/templates/example')
const execa = require('execa')
const fs = require('fs-extra')
const logSymbols = require('log-symbols')
const { paramCase } = require('param-case')
const { pascalCase } = require('pascal-case')
const reactNativeInit = require('react-native-init-func')
const updateNotifier = require('update-notifier')
const BULB = bulb
const INFO = logSymbols.info
const OK = logSymbols.success
const WARN = logSymbols.warning
const ERROR = logSymbols.error
// used in quick workaround for
// https://github.com/terkelg/prompts/issues/252
const SHOW_CURSOR = ansiEscapes.cursorShow
const REACT_NATIVE_PREFIX = 'react-native-'
const EXAMPLE_APP_JS_FILENAME = 'App.js'
// rewrite metro.config.js with workaround solutions
const EXAMPLE_METRO_CONFIG_FILENAME = 'metro.config.js'
const EXAMPLE_METRO_CONFIG_WORKAROUND = `// metro.config.js
// with workaround solutions
const path = require('path')
module.exports = {
// workaround for issue with symlinks encountered starting with
// metro@0.55 / React Native 0.61
// (not needed with React Native 0.60 / metro@0.54)
resolver: {
extraNodeModules: new Proxy(
{},
{ get: (_, name) => path.resolve('.', 'node_modules', name) }
)
},
// quick workaround solution for issue with symlinked modules ref:
// https://github.com/brodybits/create-react-native-module/issues/232
watchFolders: [path.resolve('.'), path.resolve('..')]
}
`
// resolve the process cwd for once
// equivalent to using process.cwd ref:
// https://stackoverflow.com/a/9874415
// (resolve('.') is a little easier to mock at this point)
const cwdPath = path.resolve('.')
// path helpers:
const resolveSubpath = (...paths) => path.resolve(...paths)
const joinPath = path.join
// quick workaround ref:
// https://github.com/terkelg/prompts/issues/252
function onPromptState ({ aborted }) {
if (aborted) {
// should put SHOW_CURSOR to stdout & end with a newline ref:
// - https://stackoverflow.com/questions/4976466/difference-between-process-stdout-write-and-console-log-in-node-js/4984464#4984464
// - https://github.com/nodejs/node/blob/v14.x/lib/internal/console/constructor.js
// these npm package calls can be easily mocked and tested with Jest
log(SHOW_CURSOR)
exit(1)
}
}
// with quick workaround for
// https://github.com/terkelg/prompts/issues/252
function prompt (props) {
return prompts([{ onState: onPromptState, ...props }])
}
async function promptForConfirmation (message) {
const { confirmation } = await prompt({
type: 'confirm',
name: 'confirmation',
message,
initial: true
})
if (!confirmation) exit(1)
}
// notify the user (...)
const notifier = updateNotifier({ pkg })
notifier.notify()
// using Promise.resolve().then(...) to avoid possible issue with
// IIFE directly after expression with no semicolon
Promise.resolve().then(async () => {
// Show the tool info:
log(INFO, pkg.name, pkg.version)
const { nativeModuleName } = await prompt({
type: 'text',
name: 'nativeModuleName',
message: 'What is the desired native module name?',
validate: nativeModuleName =>
nativeModuleName.length > 0 && paramCase(nativeModuleName).length > 0
})
const nameParamCase = paramCase(nativeModuleName)
const namePascalCase = pascalCase(nativeModuleName)
const { isView } = await prompt({
type: 'toggle',
name: 'isView',
message: 'Should it be a view?',
initial: false,
active: 'yes',
inactive: 'no'
})
log(INFO, `OK, continuing with isView: ${isView}`)
const initialModulePackageName = nameParamCase.startsWith(REACT_NATIVE_PREFIX)
? nameParamCase
: REACT_NATIVE_PREFIX.concat(nameParamCase)
const { modulePackageName } = await prompt({
type: 'text',
name: 'modulePackageName',
message: `What is the full native ${
isView ? 'view' : 'module'
} package name?`,
initial: initialModulePackageName,
validate: modulePackageName => modulePackageName.length > 0
})
// FUTURE TBD it should be possible for the user to enter a different
// inital package version value to start with
await promptForConfirmation('Initial package version is 1.0.0 - continue?')
const { nativeObjectClassNamePrefixInput } = await prompt({
type: 'text',
name: 'nativeObjectClassNamePrefixInput',
message:
'What is the desired native object class name prefix (can be blank)?',
initial: ''
})
// quick solution to get the prefix in upper case, in a way that can
// be part of a native class name (with no symbols, etc.)
const nativeObjectClassNamePrefix = pascalCase(
nativeObjectClassNamePrefixInput
).toUpperCase()
const { nativeObjectClassName } = await prompt({
type: 'text',
name: 'nativeObjectClassName',
message:
'Desired object class name to use between JavaScript & native code?',
initial: nativeObjectClassNamePrefix.concat(namePascalCase)
})
const { platforms } = await prompt({
type: 'multiselect',
name: 'platforms',
message: 'Which native platforms?',
choices: [
{ title: 'Android', value: 'android', selected: true },
{ title: 'iOS', value: 'ios', selected: true },
{ title: 'Windows', value: 'windows', disabled: true }
],
min: 1
})
const { androidPackageId } =
platforms.indexOf('android') !== -1
? await prompt({
type: 'text',
name: 'androidPackageId',
message: 'What is the desired Android package id?',
initial: 'com.demo',
validate: androidPackageId => androidPackageId.length > 0
})
: { androidPackageId: null }
const { tvosEnabled } =
platforms.indexOf('ios') !== -1
? await prompt({
type: 'confirm',
name: 'tvosEnabled',
message: 'Support Apple tvOS (requires react-native-tvos fork)?',
initial: false
})
: { tvosEnabled: null }
// THANKS to @react-native-community/bob for the idea
// to get user name & email from git
const gitUserName = (await execa('git', ['config', 'user.name'])).stdout
const gitUserEmail = (await execa('git', ['config', 'user.email'])).stdout
const { authorName } = await prompt({
type: 'text',
name: 'authorName',
message: 'What is the author name?',
initial: gitUserName
})
const { authorEmail } = await prompt({
type: 'text',
name: 'authorEmail',
message: 'What is the author email?',
initial: gitUserEmail
})
const { githubUserAccountName } = await prompt({
type: 'text',
name: 'githubUserAccountName',
message: 'What is the GitHub user account name?',
initial: authorEmail.split('@')[0]
})
const { license } = await prompt({
type: 'text',
name: 'license',
message: 'What license?',
initial: 'MIT'
})
const { useAppleNetworking } =
platforms.indexOf('ios') !== -1 && !isView
? await prompt({
type: 'confirm',
name: 'useAppleNetworking',
message: 'Generate with sample use of Apple Networking?',
initial: false
})
: { useAppleNetworking: false }
log(INFO, 'It is possible to generate an example app for testing,')
log(INFO, 'with workarounds in metro.config.js for metro linking issues')
log(INFO, 'Requirements: Yarn; pod is needed for iOS')
const { generateExampleApp } = await prompt({
type: 'confirm',
name: 'generateExampleApp',
message: 'Generate the example app (with workarounds in metro.config.js)?',
initial: true
})
const exampleAppName = generateExampleApp
? (
await prompt({
type: 'text',
name: 'exampleAppName',
message: 'Example app name?',
initial: 'example'
})
).exampleAppName
: null
const exampleTemplate = generateExampleApp
? (
await prompt({
type: 'text',
name: 'exampleTemplate',
message: `What react-native template to use for the example app (should be for at least ${
tvosEnabled ? 'react-native-tvos@0.60' : 'react-native@0.60'
})?`,
initial: tvosEnabled
? 'react-native-tvos@latest'
: 'react-native@latest'
})
).exampleTemplate
: null
if (generateExampleApp) {
log(INFO, 'checking that Yarn CLI can show its version')
try {
await execa('yarn', ['--version'])
} catch (e) {
log(ERROR, 'Yarn CLI not installed correctly')
process.exit(1)
}
log(OK, 'Yarn CLI ok')
}
log(INFO, 'generating the native library module as a package')
const createOptions = {
name: nativeModuleName,
packageName: modulePackageName,
isView,
objectClassName: nativeObjectClassName,
nativePackageId: androidPackageId,
platforms,
tvosEnabled,
authorName,
authorEmail,
githubAccount: githubUserAccountName,
license,
useAppleNetworking
}
await createReactNativeLibraryModule(createOptions)
log(OK, 'native library module generated ok')
if (generateExampleApp) {
log(INFO, 'generating the example app')
const exampleAppTemplate = exampleTemplates.slice(-1)[0]
// Note that paths must be determined before calling reactNativeInit which
// seems to change the process cwd as of react-native-init-func 0.0.1
const modulePath = resolveSubpath(cwdPath, modulePackageName)
const exampleAppPath = resolveSubpath(modulePath, exampleAppName)
const exampleAppSubdirectory = joinPath(modulePackageName, exampleAppName)
await reactNativeInit([exampleAppName], {
directory: exampleAppSubdirectory,
template: exampleTemplate
})
log(INFO, 'generating App.js in the example app')
await fs.outputFile(
resolveSubpath(exampleAppPath, EXAMPLE_APP_JS_FILENAME),
exampleAppTemplate.content({
...createOptions,
name: nativeObjectClassName
})
)
// rewrite metro.config.js with workaround solutions
log(
INFO,
`rewrite ${EXAMPLE_METRO_CONFIG_FILENAME} with workaround solutions`
)
await fs.outputFile(
resolveSubpath(exampleAppPath, EXAMPLE_METRO_CONFIG_FILENAME),
EXAMPLE_METRO_CONFIG_WORKAROUND
)
log(OK, 'example app generated ok')
log(
INFO,
'adding the native library module into the example app as a dependency link'
)
await execa('yarn', ['add', 'link:../'], {
cwd: exampleAppPath,
stdout: 'inherit',
stderr: 'inherit'
})
log(
OK,
'added the native library module into the example app as a dependency link - ok'
)
if (platforms.indexOf('ios') !== -1) {
// NOTE that the React Native CLI would offer to install the pod tool,
// if needed (on macOS)
log(INFO, 'checking that the pod tool can show its version')
try {
await execa('pod', ['--version'])
} catch (e) {
log(ERROR, 'pod tool not installed correctly')
process.exit(1)
}
log(OK, 'pod tool ok')
log(
INFO,
'starting additional pod install in ios subdirectory of example app'
)
try {
await execa('pod', ['install'], {
cwd: resolveSubpath(exampleAppPath, 'ios'),
stdout: 'inherit',
stderr: 'inherit'
})
} catch (e) {
process.exit(1)
}
log(OK, 'additional pod install ok')
}
// show the example app info:
log(BULB, `check out the example app in ${exampleAppSubdirectory}`)
log(INFO, `(${exampleAppPath})`)
log(BULB, 'recommended: run Metro Bundler in a new shell')
log(INFO, `(cd ${exampleAppSubdirectory} && yarn start)`)
log(BULB, 'enter the following commands to run the example app:')
log(INFO, `cd ${exampleAppSubdirectory}`)
platforms.forEach(p => {
log(INFO, `yarn ${p} # for React Native 0.60: npx react-native run-${p}`)
})
// show first steps in case of a clean checkout:
const iosSubdirectory = joinPath(exampleAppSubdirectory, 'ios')
log(WARN, 'first steps in case of a clean checkout')
log(INFO, `run Yarn in ${exampleAppSubdirectory}`)
log(INFO, `(cd ${exampleAppSubdirectory} && yarn)`)
log(INFO, `do \`pod install\` for iOS in ${iosSubdirectory}`)
log(INFO, `(cd ${iosSubdirectory} && pod install)`)
}
})