@wavemaker-ai/wm-reactnative-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +236 -0
  2. package/assets/CLI-EnvironmentVariable.png +0 -0
  3. package/assets/EnvironmentVariable.png +0 -0
  4. package/assets/EnvironmentVariable1.png +0 -0
  5. package/files/ui-build.js +331 -0
  6. package/index.js +381 -0
  7. package/package.json +39 -0
  8. package/src/android.js +479 -0
  9. package/src/command.js +552 -0
  10. package/src/config.js +11 -0
  11. package/src/custom-logger/progress-bar.js +97 -0
  12. package/src/custom-logger/steps.js +117 -0
  13. package/src/custom-logger/task-logger.js +147 -0
  14. package/src/exec.js +73 -0
  15. package/src/expo-launcher.js +596 -0
  16. package/src/ios.js +517 -0
  17. package/src/logger.js +104 -0
  18. package/src/mobileprovision-parse/index.js +72 -0
  19. package/src/project-sync.service.js +390 -0
  20. package/src/requirements.js +250 -0
  21. package/src/utils.js +100 -0
  22. package/src/web-preview-launcher.js +548 -0
  23. package/src/zip.js +19 -0
  24. package/templates/embed/android/ReactNativeAppFragment.java +78 -0
  25. package/templates/embed/android/SplashScreenReactActivityLifecycleListener.kt +41 -0
  26. package/templates/embed/android/fragment_react_native_app.xml +14 -0
  27. package/templates/embed/ios/ReactNativeView.h +12 -0
  28. package/templates/embed/ios/ReactNativeView.m +59 -0
  29. package/templates/embed/ios/ReactNativeView.swift +53 -0
  30. package/templates/expo-camera-patch/useWebQRScanner.js +100 -0
  31. package/templates/ios-build-patch/podFIlePostInstall.js +72 -0
  32. package/templates/package/packageLock.json +14334 -0
  33. package/templates/wm-rn-runtime/App.js +479 -0
  34. package/templates/wm-rn-runtime/App.navigator.js +109 -0
  35. package/test.js +0 -0
  36. package/tools-site/index.html.template +17 -0
  37. package/tools-site/page_background.svg +99 -0
  38. package/tools-site/qrcode.js +614 -0
  39. package/tools-site/styles.css +39 -0
package/src/command.js ADDED
@@ -0,0 +1,552 @@
1
+ const fs = require('fs-extra');
2
+ const logger = require('./logger');
3
+ const plist = require('plist');
4
+ const path = require('path');
5
+ const android = require('./android');
6
+ const { unzip } = require('./zip');
7
+ let { showConfirmation,VERSIONS,
8
+ canDoAndroidBuild, canDoIosBuild, canDoEmbed
9
+ } = require('./requirements');
10
+
11
+ const {
12
+ exec
13
+ } = require('./exec');
14
+
15
+ const crypto = require('crypto');
16
+ const config = require('./config');
17
+ const ios = require('./ios');
18
+ const { resolve } = require('path');
19
+ const { isWindowsOS, readAndReplaceFileContent, getDestPathForWindows, updateIsAiPlatform } = require('./utils');
20
+ const chalk = require('chalk');
21
+ const taskLogger = require('./custom-logger/task-logger').spinnerBar;
22
+ const loggerLabel = 'wm-reactnative-cli';
23
+ const {androidBuildSteps} = require('./custom-logger/steps');
24
+
25
+ function getFileSize(path) {
26
+ const stats = path && fs.statSync(path);
27
+ return (stats && stats['size']) || 0;
28
+ }
29
+
30
+ async function updatePackageJsonFile(path) {
31
+ try {
32
+ let data = fs.readFileSync(path, 'utf-8');
33
+ //downgrading expo-av to 11 to address the build failure issue
34
+ data = data.replace(/"expo-av"[\s]*:[\s]*"~13.0.1"/, '"expo-av": "~11.0.1"');
35
+ const jsonData = JSON.parse(data);
36
+ jsonData['main'] = "index";
37
+ if (config.embed) {
38
+ jsonData['dependencies']['@wavemaker/expo-native-module'] = "latest";
39
+ }
40
+ if(!jsonData['devDependencies']['@babel/plugin-proposal-optional-chaining']){
41
+ jsonData['devDependencies']['@babel/plugin-proposal-optional-chaining'] = "^7.21.0";
42
+ }
43
+ if(!jsonData['devDependencies']['@babel/plugin-proposal-nullish-coalescing-operator']){
44
+ jsonData['devDependencies']['@babel/plugin-proposal-nullish-coalescing-operator'] = "^7.18.6";
45
+ }
46
+ if (!jsonData['dependencies']['lottie-react-native']
47
+ || jsonData['dependencies']['lottie-react-native'] === '5.1.5') {
48
+ jsonData['dependencies']['lottie-react-native'] = "^5.1.5";
49
+ jsonData['dependencies']['react-lottie-player'] = "^1.5.4";
50
+ }
51
+ if (jsonData['dependencies']['expo-file-system'] === '^15.1.1') {
52
+ jsonData['dependencies']['expo-file-system'] = '15.2.2'
53
+ }
54
+ if (jsonData['dependencies']['axios'] === '^1.4.0') {
55
+ jsonData['dependencies']['axios'] = '1.6.8';
56
+ }
57
+ const resolutions = jsonData["resolutions"] || {};
58
+ if (!resolutions['expo-application']) {
59
+ resolutions['expo-application'] = '5.8.4';
60
+ }
61
+ if (!resolutions['axios']) {
62
+ resolutions['axios'] = '1.6.8';
63
+ }
64
+ if (jsonData['dependencies']['expo'] === '50.0.17') {
65
+ resolutions['metro'] = '0.80.9';
66
+ }
67
+ jsonData["resolutions"] = resolutions;
68
+ if (config.platform === 'android') {
69
+ jsonData['dependencies']['@react-native-cookies/cookies'] = '6.2.1';
70
+ }
71
+ fs.writeFileSync(path, JSON.stringify(jsonData), 'utf-8');
72
+ logger.info({
73
+ 'label': loggerLabel,
74
+ 'message': 'updated package.json file'
75
+ });
76
+ } catch (e) {
77
+ resolve('error', e);
78
+ }
79
+ }
80
+
81
+ async function build(args) {
82
+ const directories = await setupBuildDirectory(args.src, args.dest, args.platform);
83
+ if (!directories) {
84
+ return {
85
+ success : false,
86
+ errors: 'could not setup the build directories.'
87
+ };
88
+ }
89
+ args.src = directories.src;
90
+ args.dest = directories.dest;
91
+
92
+ config.metaData = await readWmRNConfig(args.src);
93
+ const packageJsonData = await readPackageJson(args.src);
94
+
95
+ const deps = packageJsonData.dependencies || {};
96
+ const is_ai_platform = Boolean(
97
+ deps['@wavemaker-ai/rn-codegen'] ||
98
+ deps['@wavemaker-ai/app-rn-runtime']
99
+ );
100
+ updateIsAiPlatform(is_ai_platform);
101
+
102
+ if (config.metaData.icon.src.startsWith('resources')) {
103
+ config.metaData.icon.src = 'assets/' + config.metaData.icon.src;
104
+ }
105
+ if (config.metaData.splash.src?.startsWith('resources')) {
106
+ config.metaData.splash.src = 'assets/' + config.metaData.splash.src;
107
+ }
108
+
109
+ config.platform = args.platform;
110
+
111
+ if (args.dest) {
112
+ args.dest = path.resolve(args.dest) + '/';
113
+ }
114
+ taskLogger.succeed(androidBuildSteps[0].succeed);
115
+
116
+ await prepareProject(args);
117
+ if (args.targetPhase === 'PREPARE')
118
+ {
119
+ return;
120
+ }
121
+ if (!args.autoEject) {
122
+ const response = await showConfirmation(
123
+ 'Would you like to eject the expo project (yes/no) ?'
124
+ );
125
+ if (response !== 'y' && response !== 'yes') {
126
+ process.exit();
127
+ }
128
+ }
129
+
130
+ let response;
131
+ if (args.dest) {
132
+ if (!config.metaData.ejected) {
133
+ response = await ejectProject(args);
134
+ }
135
+ } else {
136
+ response = await ejectProject(args);
137
+ }
138
+
139
+ if (response && response.errors) {
140
+ return response;
141
+ }
142
+
143
+ if (args.ejectProject || config.embed) {
144
+ return;
145
+ }
146
+
147
+ if (args.dest) {
148
+ config.src = args.dest;
149
+ }
150
+ // TODO: iOS app showing blank screen
151
+ if (!(config.metaData.sslPinning && config.metaData.sslPinning.enabled)) {
152
+ await readAndReplaceFileContent(`${config.src}/App.js`, content => {
153
+ return content.replace('if (isSslPinningAvailable()) {',
154
+ 'if (false && isSslPinningAvailable()) {');
155
+ });
156
+ }
157
+
158
+ if(args.architecture && args.platform==='android') {
159
+ await readAndReplaceFileContent(`${config.src}/android/gradle.properties`, content => {
160
+ return content.replace(/^reactNativeArchitectures=.*$/m,`reactNativeArchitectures=${args.architecture.join(',')}`);
161
+ })
162
+ }
163
+
164
+ config.outputDirectory = config.src + 'output/';
165
+ config.logDirectory = config.outputDirectory + 'logs/';
166
+ logger.info({
167
+ label: loggerLabel,
168
+ message: `Building at : ${config.src}`
169
+ });
170
+
171
+ taskLogger.info(`Building at : ${config.src}`);
172
+
173
+ try {
174
+ let result;
175
+ // await clearUnusedAssets(config.platform);
176
+ if (config.platform === 'android') {
177
+ result = await android.invokeAndroidBuild(args);
178
+ } else if (config.platform === 'ios') {
179
+ try{
180
+ taskLogger.start("Installing pods....")
181
+ await exec('pod', ['install'], {
182
+ cwd: config.src + 'ios'
183
+ });
184
+ }catch(e){
185
+ taskLogger.fail("Pod install failed");
186
+ }
187
+ result = await ios.invokeiosBuild(args);
188
+ }
189
+ if (result.errors && result.errors.length) {
190
+ logger.error({
191
+ label: loggerLabel,
192
+ message: args.platform + ' build failed due to: \n\t' + result.errors.join('\n\t')
193
+ });
194
+ taskLogger.fail(args.platform + ' build failed due to: \n\t' + result.errors.join('\n\t'));
195
+ } else if (!result.success) {
196
+ logger.error({
197
+ label: loggerLabel,
198
+ message: args.platform + ' BUILD FAILED'
199
+ });
200
+ taskLogger.fail(args.platform + ' BUILD FAILED');
201
+ } else {
202
+ logger.info({
203
+ label: loggerLabel,
204
+ message: `${args.platform} BUILD SUCCEEDED. check the file at : ${result.output}.`
205
+ });
206
+ taskLogger.info(`${args.platform} BUILD SUCCEEDED. check the file at : ${result.output}.`);
207
+ logger.info({
208
+ label: loggerLabel,
209
+ message: `File size : ${Math.round(getFileSize(result.output) * 100 / (1024 * 1024)) / 100} MB.`
210
+ });
211
+ taskLogger.info(`File size : ${Math.round(getFileSize(result.output) * 100 / (1024 * 1024)) / 100} MB.`);
212
+ }
213
+ return result;
214
+ } catch(e) {
215
+ logger.error({
216
+ label: loggerLabel,
217
+ message: 'BUILD Failed. Due to :' + e
218
+ });
219
+ taskLogger.fail('BUILD Failed. Due to :' + e);
220
+ return {
221
+ success : false,
222
+ errors: e
223
+ };
224
+ }
225
+ }
226
+
227
+ async function extractRNZip(src) {
228
+ let folderName = isWindowsOS() ? src.split('\\').pop() : src.split('/').pop();
229
+ const isZipFile = folderName.endsWith('.zip');
230
+
231
+ folderName = isZipFile ? folderName.replace('.zip', '') : folderName;
232
+
233
+ const tmp = `${require('os').homedir()}/.wm-reactnative-cli/temp/${folderName}/${Date.now()}`;
234
+
235
+ if (src.endsWith('.zip')) {
236
+ const zipFile = src;
237
+ src = tmp + '/src';
238
+
239
+ if (!fs.existsSync(src)) {
240
+ fs.mkdirsSync(src);
241
+ }
242
+ await unzip(zipFile, src);
243
+ }
244
+ return path.resolve(src) + '/';
245
+ }
246
+
247
+ async function setupBuildDirectory(src, dest, platform) {
248
+ try{
249
+ taskLogger.setTotal(androidBuildSteps[0].total);
250
+ taskLogger.start(androidBuildSteps[0].start);
251
+ src = await extractRNZip(src);
252
+ taskLogger.incrementProgress(1);
253
+ const metadata = await readWmRNConfig(src);
254
+ taskLogger.incrementProgress(1);
255
+ if (fs.existsSync(dest)) {
256
+ if (fs.readdirSync(dest).length) {
257
+ const response = await showConfirmation('Would you like to empty the dest folder (i.e. ' + dest + ') (yes/no) ?');
258
+ if (response !== 'y' && response !== 'yes') {
259
+ // logger.error({
260
+ // label: loggerLabel,
261
+ // message: 'Non empty folder cannot be used as desination. Please choose a different destination and build again.'
262
+ // });
263
+ // return;
264
+ }else{
265
+ // using removeSync when dest is directory and unlinkSync works when dest is file.
266
+ const fsStat = fs.lstatSync(dest);
267
+ if (fsStat.isDirectory()) {
268
+ fs.removeSync(dest);
269
+ } else if (fsStat.isFile()) {
270
+ fs.unlinkSync(dest);
271
+ }
272
+ }
273
+ }
274
+ }
275
+ taskLogger.incrementProgress(1);
276
+ dest = dest || await getDefaultDestination(metadata.id, platform);
277
+ if(isWindowsOS()){
278
+ dest = await getDestPathForWindows('build');
279
+ }
280
+ dest = path.resolve(dest) + '/';
281
+ if(src === dest) {
282
+ logger.error({
283
+ label: loggerLabel,
284
+ message: 'source and destination folders are same. Please choose a different destination.'
285
+ });
286
+ taskLogger.fail('source and destination folders are same. Please choose a different destination.');
287
+ return;
288
+ }
289
+ taskLogger.incrementProgress(1);
290
+ fs.mkdirsSync(dest);
291
+ fs.copySync(src, dest);
292
+ taskLogger.incrementProgress(1);
293
+ const logDirectory = dest + 'output/logs/';
294
+ fs.mkdirSync(logDirectory, {
295
+ recursive: true
296
+ });
297
+ global.logDirectory = logDirectory;
298
+ logger.setLogDirectory(logDirectory);
299
+ taskLogger.info("Full log details can be found in: " + logDirectory);
300
+ return {
301
+ src: src,
302
+ dest: dest
303
+ };
304
+ }catch(e){
305
+ console.log(e.message);
306
+ taskLogger.fail("Setup directories failed. " + chalk.gray("Due to : ") + chalk.cyan(e.message));
307
+ }
308
+ }
309
+
310
+ async function getDefaultDestination(id, platform) {
311
+ const version = '1.0.0';
312
+ const path = `${require('os').homedir()}/.wm-reactnative-cli/build/${id}/${version}/${platform}`;
313
+ fs.mkdirSync(path, {
314
+ recursive: true
315
+ });
316
+ let next = 1;
317
+ if (fs.existsSync(path)) {
318
+ next = fs.readdirSync(path).reduce((a, f) => {
319
+ try {
320
+ const c = parseInt(f);
321
+ if (a <= c) {
322
+ return c + 1;
323
+ }
324
+ } catch(e) {
325
+ //not a number
326
+ }
327
+ return a;
328
+ }, next);
329
+ }
330
+ const dest = path + '/' + next;
331
+ fs.mkdirSync(dest, {
332
+ recursive: true
333
+ });
334
+ return dest;
335
+ }
336
+
337
+ async function readWmRNConfig(src) {
338
+ src = path.resolve(src) + '/';
339
+ let jsonPath = src + 'wm_rn_config.json';
340
+ let data = await fs.readFileSync(jsonPath);
341
+ data = JSON.parse(data);
342
+ data.preferences = data.preferences || {};
343
+ data.preferences.enableHermes = true;
344
+ return data;
345
+ }
346
+
347
+ async function readPackageJson(src){
348
+ src = path.resolve(src) + '/';
349
+ let packageJsonPath = src + 'package.json';
350
+ let data = await fs.readFileSync(packageJsonPath);
351
+ data = JSON.parse(data);
352
+ return data;
353
+ }
354
+
355
+ async function writeWmRNConfig(content) {
356
+ src = path.resolve(config.src) + '/';
357
+ let jsonPath = src + 'wm_rn_config.json';
358
+ let data = await fs.readFileSync(jsonPath);
359
+ data = JSON.parse(data);
360
+ if (content) {
361
+ Object.assign(data, content);
362
+ }
363
+ await fs.writeFile(jsonPath, JSON.stringify(data), error => {
364
+ if (error) {
365
+ throw error;
366
+ }
367
+ logger.info({
368
+ 'label': loggerLabel,
369
+ 'message': 'updated wm_rn_config.json file'
370
+ })
371
+ })
372
+ }
373
+
374
+ // src points to unzip proj
375
+ async function ejectProject(args) {
376
+ try {
377
+ taskLogger.start(androidBuildSteps[3].start);
378
+ taskLogger.setTotal(androidBuildSteps[3].total);
379
+ taskLogger.incrementProgress(1);
380
+ if(args.platform){
381
+ await exec('npx', ['expo','prebuild', "--platform", args.platform], {
382
+ cwd: config.src
383
+ });
384
+ }else{
385
+ await exec('npx', ['expo','prebuild'], {
386
+ cwd: config.src
387
+ });
388
+ }
389
+ taskLogger.incrementProgress(1);
390
+ logger.info({
391
+ label: loggerLabel,
392
+ message: 'expo eject succeeded',
393
+ });
394
+ if (args.localrnruntimepath) {
395
+ const linkFolderPath =
396
+ config.src + `node_modules/${global.WM_REPO_SCOPE}/app-rn-runtime`;
397
+ // using removeSync when target is directory and unlinkSync works when target is file.
398
+ if (fs.existsSync(linkFolderPath)) {
399
+ fs.removeSync(linkFolderPath);
400
+ }
401
+ await fs.mkdirsSync(linkFolderPath);
402
+ await fs.copySync(args.localrnruntimepath, linkFolderPath);
403
+ logger.info({
404
+ label: loggerLabel,
405
+ message: 'copied the app-rn-runtime folder',
406
+ });
407
+ taskLogger.info("copied the app-rn-runtime folder");
408
+ }
409
+ taskLogger.succeed(androidBuildSteps[3].succeed);
410
+ } catch (e) {
411
+ logger.error({
412
+ label: loggerLabel,
413
+ message: args.platform + ' eject project Failed. Due to :' + e,
414
+ });
415
+ taskLogger.fail(androidBuildSteps[3].fail);
416
+ return { errors: e, success: false };
417
+ }
418
+ }
419
+
420
+ async function prepareProject(args) {
421
+ try {
422
+ taskLogger.setTotal(androidBuildSteps[1].total);
423
+ taskLogger.start(androidBuildSteps[1].start);
424
+ config.src = args.dest;
425
+ logger.info({
426
+ label: loggerLabel,
427
+ message: 'destination folder where app is build at ' + args.dest,
428
+ });
429
+ taskLogger.info('destination folder where app is build at ' + args.dest);
430
+ if (!args.platform) {
431
+ args.platform = 'android';
432
+ }
433
+ config.platform = args.platform;
434
+ config.buildType = args.buildType;
435
+
436
+ if (args.platform !== 'android') {
437
+ VERSIONS.JAVA = '1.8.0';
438
+ }
439
+ const prerequisiteError = {
440
+ errors: 'check if all prerequisites are installed.',
441
+ success: false
442
+ };
443
+ if (config.embed) {
444
+ if (!await canDoEmbed()) {
445
+ return prerequisiteError;
446
+ }
447
+ }
448
+ if (args.platform === 'android') {
449
+ if (!await canDoAndroidBuild()) {
450
+ return prerequisiteError;
451
+ }
452
+ }
453
+ if (args.platform === 'ios') {
454
+ if (!await canDoIosBuild()) {
455
+ return prerequisiteError;
456
+ }
457
+ }
458
+ taskLogger.incrementProgress(1);
459
+ taskLogger.succeed(androidBuildSteps[1].succeed);
460
+ taskLogger.setTotal(androidBuildSteps[2].total);
461
+ taskLogger.start(androidBuildSteps[2].start);
462
+ logger.info({
463
+ label: loggerLabel,
464
+ message: 'app.json updated.... ' + args.dest
465
+ })
466
+ taskLogger.incrementProgress(0.2);
467
+ try{
468
+ await exec('npm', ['install'], {
469
+ cwd: config.src
470
+ });
471
+ taskLogger.succeed("All dependencies installed successfully.")
472
+ }catch(e){
473
+ logger.error({
474
+ label: loggerLabel,
475
+ message: "Dependency installation failed. Due to : "+ e,
476
+ });
477
+ taskLogger.fail("Dependency installation failed. Due to : "+ e);
478
+ }
479
+ } catch (e) {
480
+ logger.error({
481
+ label: loggerLabel,
482
+ message: args.platform + ' prepare project Failed. Due to :' + e,
483
+ });
484
+ taskLogger.fail(args.platform + ' prepare project Failed. Due to :' + e);
485
+ return { errors: e, success : false };
486
+ }
487
+ }
488
+
489
+ async function clearUnusedAssets(platform) {
490
+ await readAndReplaceFileContent(config.src + 'app.theme.js', (content) => {
491
+ if (platform === 'ios') {
492
+ return content.replace(/ios:\s\{(.|\n)*?\},?/gm, ``);
493
+ }
494
+ return content.replace(/android:\s\{(.|\n)*?\},?/gm, ``);
495
+ });
496
+ logger.info({
497
+ 'label': loggerLabel,
498
+ 'message': '***** updated theme related files based on selected platform ...***'
499
+ });
500
+
501
+ const folderToExclude = platform === 'android' ? 'ios' : 'android';
502
+ const path = config.src + 'theme/' + folderToExclude;
503
+ if (fs.existsSync(path)) {
504
+ const fsStat = fs.lstatSync(path);
505
+ if (fsStat.isDirectory()) {
506
+ fs.removeSync(path);
507
+ } else if (fsStat.isFile()) {
508
+ fs.unlinkSync(path);
509
+ }
510
+ logger.info({
511
+ 'label': loggerLabel,
512
+ 'message': '***** Removed the unused platform theme folder ...***'
513
+ });
514
+ }
515
+ }
516
+
517
+ module.exports = {
518
+ ejectProject: (args) => {
519
+ args.autoEject = true;
520
+ args.ejectProject = true;
521
+ args.platform === 'expo'
522
+ build(args);
523
+ },
524
+ embed: async (args) => {
525
+ args.autoEject = true;
526
+ config.embed = true;
527
+ await build(args);
528
+ if (args.platform === 'android') {
529
+ await android.embed(args);
530
+ logger.info({
531
+ label: loggerLabel,
532
+ message: `Build Success. Check the embedded project at : ${args.dest}android-embed.`
533
+ });
534
+ } else if (args.platform === 'ios') {
535
+ await ios.embed(args);
536
+ logger.info({
537
+ label: loggerLabel,
538
+ message: `Build Success. Check the embedded project at : ${args.dest}ios-embed.`
539
+ });
540
+ }
541
+ },
542
+ build: build,
543
+ prepareProject: async (args) => {
544
+ args.targetPhase = 'PREPARE';
545
+ args.platform= 'expo';
546
+ await build(args);
547
+ logger.info({
548
+ label: loggerLabel,
549
+ message: `Project is prepared at : ${args.dest}.`,
550
+ });
551
+ },
552
+ };
package/src/config.js ADDED
@@ -0,0 +1,11 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+
4
+ module.exports = {
5
+ src: '',
6
+ buildType: '',
7
+ logDirectory: '',
8
+ outputDirectory: '',
9
+ metaData: {},
10
+ embed: false
11
+ };
@@ -0,0 +1,97 @@
1
+ const chalk = require('chalk');
2
+ const readline = require("readline");
3
+
4
+ class ProgressBar {
5
+ constructor(options = {}) {
6
+ this.showProgressBar = options.showProgressBar || false;
7
+ this.barCompleteChar = options.barCompleteChar || '█';
8
+ this.barIncompleteChar = options.barIncompleteChar || '░';
9
+ this.barWidth = options.barWidth || 20;
10
+ this.barFormat = options.barFormat || '[{bar}] {percentage}%';
11
+ this.total = options.total || 100;
12
+ this.value = 0;
13
+ this.startTime = null;
14
+
15
+ // Optional color configurations
16
+ this.completeColor = options.completeColor || null;
17
+ this.incompleteColor = options.incompleteColor || null;
18
+ this.textColor = options.textColor || null;
19
+ }
20
+
21
+ start() {
22
+ this.startTime = Date.now();
23
+ }
24
+
25
+ setProgress(value) {
26
+ this.value = Math.min(Math.max(0, value), this.total);
27
+ }
28
+
29
+ incrementProgress(amount = 1) {
30
+ this.setProgress(this.value + amount);
31
+ }
32
+
33
+ setTotal(total) {
34
+ this.total = total;
35
+ }
36
+
37
+ enable() {
38
+ this.showProgressBar = true;
39
+ }
40
+
41
+ disable() {
42
+ this.showProgressBar = false;
43
+ }
44
+
45
+ status() {
46
+ return this.showProgressBar;
47
+ }
48
+
49
+ calculateETA() {
50
+ if (!this.startTime || this.value === 0) return '?';
51
+ const elapsedTime = (Date.now() - this.startTime) / 1000;
52
+ const itemsPerSecond = this.value / elapsedTime;
53
+ const eta = Math.round((this.total - this.value) / itemsPerSecond);
54
+ return isFinite(eta) ? eta : '?';
55
+ }
56
+
57
+ render() {
58
+ if (!this.showProgressBar) return '';
59
+ const percentage = Math.floor((this.value / this.total) * 100);
60
+ const completeLength = Math.round((this.value / this.total) * this.barWidth);
61
+ const incompleteLength = this.barWidth - completeLength;
62
+
63
+ let completeBar = this.barCompleteChar.repeat(completeLength);
64
+ let incompleteBar = this.barIncompleteChar.repeat(incompleteLength);
65
+
66
+ if (this.completeColor) completeBar = chalk[this.completeColor](completeBar);
67
+ if (this.incompleteColor) incompleteBar = chalk[this.incompleteColor](incompleteBar);
68
+
69
+ let bar = completeBar + incompleteBar;
70
+ let formattedText = this.barFormat
71
+ .replace('{bar}', bar)
72
+ .replace('{percentage}', percentage)
73
+ .replace('{value}', this.value)
74
+ .replace('{total}', this.total)
75
+ .replace('{eta}', this.calculateETA());
76
+
77
+ if (this.textColor){
78
+ formattedText = chalk[this.textColor](formattedText);
79
+ }
80
+ readline.cursorTo(process.stdout, 0);
81
+
82
+ return formattedText;
83
+ }
84
+ }
85
+
86
+ const overallProgressBar = new ProgressBar({
87
+ showProgressBar: true,
88
+ barWidth: 40,
89
+ completeColor: 'green',
90
+ incompleteColor: 'gray',
91
+ textColor: 'cyan'
92
+ });
93
+
94
+ module.exports = {
95
+ ProgressBar,
96
+ overallProgressBar
97
+ };