react-native-3rddigital-appupdate 1.0.11 → 1.0.12

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 (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/bundle.js +739 -153
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-3rddigital-appupdate",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "A React Native library for seamless over-the-air (OTA) updates with version checks, automatic bundle download, and customizable user prompts for iOS and Android.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
package/scripts/bundle.js CHANGED
@@ -15,17 +15,14 @@ const APPUPDATE_AWS_SECRET_ACCESS_KEY =
15
15
  process.env.APPUPDATE_AWS_SECRET_ACCESS_KEY;
16
16
  const APPUPDATE_AWS_BUCKET_NAME = process.env.APPUPDATE_AWS_BUCKET_NAME;
17
17
 
18
- /**
19
- * Decrypt helper logic
20
- */
21
18
  function DecriptEnv(wrappedKey) {
22
19
  if (!wrappedKey) return '';
23
20
  if (typeof wrappedKey !== 'string')
24
21
  throw new TypeError('wrappedKey must be a string');
25
22
  if (wrappedKey.length <= 8) throw new Error('wrappedKey too short to unwrap');
23
+
26
24
  const trimmed = wrappedKey.slice(4, -2);
27
- const result = trimmed.slice(0, 2) + trimmed.slice(4);
28
- return result;
25
+ return trimmed.slice(0, 2) + trimmed.slice(4);
29
26
  }
30
27
 
31
28
  const s3Client = new S3Client({
@@ -36,9 +33,17 @@ const s3Client = new S3Client({
36
33
  },
37
34
  });
38
35
 
39
- /**
40
- * Uploads local file to S3
41
- */
36
+ function run(command, cwd = process.cwd()) {
37
+ try {
38
+ console.log(`\n➡️ Running: ${command}\n`);
39
+ execSync(command, { stdio: 'inherit', cwd });
40
+ } catch (err) {
41
+ console.error(`❌ Command failed: ${command}`);
42
+ console.error(err.message);
43
+ process.exit(1);
44
+ }
45
+ }
46
+
42
47
  async function uploadFileToS3(filePath, bucketName, folder) {
43
48
  const fileName = path.basename(filePath);
44
49
  const cleanFileName = fileName.replace(/\s+/g, '_');
@@ -67,24 +72,6 @@ async function uploadFileToS3(filePath, bucketName, folder) {
67
72
  }
68
73
  }
69
74
 
70
- /**
71
- * Run a shell command synchronously.
72
- */
73
- function run(command) {
74
- try {
75
- console.log(`\n➡️ Running: ${command}\n`);
76
- execSync(command, { stdio: 'inherit' });
77
- } catch (err) {
78
- console.error(`❌ Command failed: ${command}`);
79
- console.error(err.message);
80
- process.exit(1);
81
- }
82
- }
83
-
84
- /**
85
- * Step 1: Upload to S3
86
- * Step 2: Register with Backend
87
- */
88
75
  async function uploadBundle({ filePath, platform, config }) {
89
76
  console.log(`📤 Starting upload process for ${platform}...`);
90
77
 
@@ -94,7 +81,6 @@ async function uploadBundle({ filePath, platform, config }) {
94
81
  }
95
82
 
96
83
  try {
97
- // 1. Upload to S3
98
84
  const s3Result = await uploadFileToS3(
99
85
  filePath,
100
86
  DecriptEnv(APPUPDATE_AWS_BUCKET_NAME),
@@ -105,12 +91,11 @@ async function uploadBundle({ filePath, platform, config }) {
105
91
 
106
92
  console.log(`✅ S3 Upload Complete: ${s3Result.Key}`);
107
93
 
108
- // 2. Prepare Payload for Backend
109
94
  const stats = fs.statSync(filePath);
110
95
  const payload = {
111
96
  projectId: config.PROJECT_ID,
112
97
  environment: config.ENVIRONMENT,
113
- platform: platform,
98
+ platform,
114
99
  version: config.VERSION,
115
100
  forceUpdate: config.FORCE_UPDATE,
116
101
  s3Key: s3Result.Key,
@@ -142,151 +127,656 @@ async function uploadBundle({ filePath, platform, config }) {
142
127
  }
143
128
  }
144
129
 
145
- /**
146
- * Build Android JS bundle and zip it.
147
- */
148
- function buildAndroid() {
149
- console.log('📦 Building Android bundle...');
150
- const outputPath = path.join('android', 'index.android.bundle.zip');
151
- run(
152
- `mkdir -p android/output && ` +
153
- `react-native bundle --platform android --dev false --entry-file index.js ` +
154
- `--bundle-output android/output/index.android.bundle --assets-dest android/output ` +
155
- `--sourcemap-output android/sourcemap.js && ` +
156
- `cd android && find output -type f | zip index.android.bundle.zip -@ && ` +
157
- `zip sourcemap.zip sourcemap.js && cd .. && rm -rf android/output && rm -rf android/sourcemap.js`
158
- );
159
- console.log(`✅ Android bundle created at ${outputPath}`);
160
- return outputPath;
130
+ function getProjectRoot() {
131
+ const cwdPackageJson = path.join(process.cwd(), 'package.json');
132
+ if (fs.existsSync(cwdPackageJson)) {
133
+ return process.cwd();
134
+ }
135
+
136
+ let projectRoot = path.resolve(__dirname);
137
+ while (
138
+ projectRoot.includes('node_modules') &&
139
+ !fs.existsSync(path.join(projectRoot, 'package.json'))
140
+ ) {
141
+ projectRoot = path.resolve(projectRoot, '..');
142
+ }
143
+
144
+ if (projectRoot.includes('node_modules')) {
145
+ projectRoot = path.resolve(projectRoot, '../../');
146
+ }
147
+
148
+ return projectRoot;
161
149
  }
162
150
 
163
- /**
164
- * Build iOS JS bundle and zip it.
165
- */
166
- function buildIOS() {
167
- console.log('📦 Building iOS bundle...');
168
- const outputPath = path.join('ios', 'main.jsbundle.zip');
169
- run(
170
- `mkdir -p ios/output && ` +
171
- `react-native bundle --platform ios --dev false --entry-file index.js ` +
172
- `--bundle-output ios/output/main.jsbundle --assets-dest ios/output ` +
173
- `--sourcemap-output ios/sourcemap.js && ` +
174
- `cd ios && find output -type f | zip main.jsbundle.zip -@ && ` +
175
- `zip sourcemap.zip sourcemap.js && cd .. && rm -rf ios/output && rm -rf ios/sourcemap.js`
176
- );
177
- console.log(`✅ iOS bundle created at ${outputPath}`);
178
- return outputPath;
151
+ function findFirstExistingPath(possiblePaths) {
152
+ return possiblePaths.find((item) => fs.existsSync(item)) ?? null;
179
153
  }
180
154
 
181
- /**
182
- * Automatically detect app version from native files.
183
- */
184
- function getAppVersion(platform) {
185
- try {
186
- // Find the actual React Native project root (two levels up from node_modules)
187
- let projectRoot = path.resolve(__dirname);
188
- while (
189
- projectRoot.includes('node_modules') &&
190
- !fs.existsSync(path.join(projectRoot, 'package.json'))
191
- ) {
192
- projectRoot = path.resolve(projectRoot, '..');
155
+ function findFirstXcodeProj(dir) {
156
+ const files = fs.readdirSync(dir);
157
+
158
+ for (const file of files) {
159
+ const fullPath = path.join(dir, file);
160
+ const stat = fs.statSync(fullPath);
161
+
162
+ if (stat.isDirectory()) {
163
+ if (file.endsWith('.xcodeproj')) return fullPath;
164
+ const nested = findFirstXcodeProj(fullPath);
165
+ if (nested) return nested;
193
166
  }
167
+ }
194
168
 
195
- // Once we exit node_modules, ensure we’re at the React Native app root
196
- if (projectRoot.includes('node_modules')) {
197
- projectRoot = path.resolve(projectRoot, '../../');
169
+ return null;
170
+ }
171
+
172
+ function walkFiles(dir, matcher, result = []) {
173
+ if (!fs.existsSync(dir)) return result;
174
+
175
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
176
+ for (const entry of entries) {
177
+ const fullPath = path.join(dir, entry.name);
178
+ if (entry.isDirectory()) {
179
+ walkFiles(fullPath, matcher, result);
180
+ continue;
198
181
  }
199
182
 
200
- if (platform === 'android') {
201
- const gradlePath = path.join(
202
- projectRoot,
203
- 'android',
204
- 'app',
205
- 'build.gradle'
206
- );
207
- if (!fs.existsSync(gradlePath)) {
208
- console.warn(`⚠️ Android build.gradle not found at ${gradlePath}`);
209
- return null;
210
- }
211
- const gradleContent = fs.readFileSync(gradlePath, 'utf8');
212
- const match = gradleContent.match(/versionName\s+"([\d.]+)"/);
213
- if (match && match[1]) {
214
- return match[1];
215
- } else {
216
- console.warn('⚠️ Could not find versionName in build.gradle.');
183
+ if (matcher(fullPath)) {
184
+ result.push(fullPath);
185
+ }
186
+ }
187
+
188
+ return result;
189
+ }
190
+
191
+ function extractBracedBlock(content, startIndex) {
192
+ const openIndex = content.indexOf('{', startIndex);
193
+ if (openIndex === -1) return null;
194
+
195
+ let depth = 0;
196
+ let inSingleQuote = false;
197
+ let inDoubleQuote = false;
198
+ let inTemplate = false;
199
+ let inLineComment = false;
200
+ let inBlockComment = false;
201
+
202
+ for (let index = openIndex; index < content.length; index += 1) {
203
+ const char = content[index];
204
+ const nextChar = content[index + 1];
205
+ const prevChar = content[index - 1];
206
+
207
+ if (inLineComment) {
208
+ if (char === '\n') inLineComment = false;
209
+ continue;
210
+ }
211
+
212
+ if (inBlockComment) {
213
+ if (prevChar === '*' && char === '/') inBlockComment = false;
214
+ continue;
215
+ }
216
+
217
+ if (!inSingleQuote && !inDoubleQuote && !inTemplate) {
218
+ if (char === '/' && nextChar === '/') {
219
+ inLineComment = true;
220
+ index += 1;
221
+ continue;
217
222
  }
218
- } else if (platform === 'ios') {
219
- const iosDir = path.join(projectRoot, 'ios');
220
- if (!fs.existsSync(iosDir)) {
221
- console.warn(`⚠️ iOS folder not found at ${iosDir}`);
222
- return null;
223
+
224
+ if (char === '/' && nextChar === '*') {
225
+ inBlockComment = true;
226
+ index += 1;
227
+ continue;
223
228
  }
229
+ }
224
230
 
225
- const projectDir = fs
226
- .readdirSync(iosDir)
227
- .find((d) => d.endsWith('.xcodeproj'));
231
+ if (!inDoubleQuote && !inTemplate && char === "'" && prevChar !== '\\') {
232
+ inSingleQuote = !inSingleQuote;
233
+ continue;
234
+ }
228
235
 
229
- if (!projectDir) {
230
- console.warn('⚠️ .xcodeproj not found inside ios directory.');
231
- return null;
236
+ if (!inSingleQuote && !inTemplate && char === '"' && prevChar !== '\\') {
237
+ inDoubleQuote = !inDoubleQuote;
238
+ continue;
239
+ }
240
+
241
+ if (!inSingleQuote && !inDoubleQuote && char === '`' && prevChar !== '\\') {
242
+ inTemplate = !inTemplate;
243
+ continue;
244
+ }
245
+
246
+ if (inSingleQuote || inDoubleQuote || inTemplate) continue;
247
+
248
+ if (char === '{') depth += 1;
249
+ if (char === '}') {
250
+ depth -= 1;
251
+ if (depth === 0) {
252
+ return {
253
+ content: content.slice(openIndex + 1, index),
254
+ start: openIndex,
255
+ end: index,
256
+ };
232
257
  }
258
+ }
259
+ }
260
+
261
+ return null;
262
+ }
263
+
264
+ function extractNamedBlock(content, blockName) {
265
+ const blockRegex = new RegExp(`\\b${blockName}\\b\\s*\\{`, 'm');
266
+ const match = blockRegex.exec(content);
267
+ if (!match) return null;
268
+ return extractBracedBlock(content, match.index);
269
+ }
270
+
271
+ function parseTopLevelNamedBlocks(content) {
272
+ const blocks = [];
273
+ let cursor = 0;
274
+
275
+ while (cursor < content.length) {
276
+ const remainder = content.slice(cursor);
277
+ const nameMatch = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*\{/.exec(remainder);
278
+
279
+ if (!nameMatch) {
280
+ cursor += 1;
281
+ continue;
282
+ }
283
+
284
+ const nameIndex = cursor + nameMatch.index;
285
+ const name = nameMatch[1];
286
+ const block = extractBracedBlock(content, nameIndex);
287
+
288
+ if (!block) break;
289
+
290
+ blocks.push({ name, content: block.content });
291
+ cursor = block.end + 1;
292
+ }
293
+
294
+ return blocks;
295
+ }
296
+
297
+ function readQuotedGradleValue(blockContent, key) {
298
+ const match = blockContent.match(
299
+ new RegExp(`\\b${key}\\b\\s+["']([^"']+)["']`)
300
+ );
301
+ return match?.[1] ?? null;
302
+ }
303
+
304
+ function cleanPbxString(value) {
305
+ if (!value) return null;
306
+ return value.replace(/^"(.*)"$/, '$1').trim();
307
+ }
308
+
309
+ function readPbxValue(body, key) {
310
+ const match = body.match(new RegExp(`\\b${key}\\s*=\\s*([^;]+);`));
311
+ return match?.[1]?.trim() ?? null;
312
+ }
313
+
314
+ function parsePbxArray(body, key) {
315
+ const match = body.match(new RegExp(`\\b${key}\\s*=\\s*\\(([\\s\\S]*?)\\);`));
316
+ if (!match) return [];
317
+
318
+ return match[1]
319
+ .split('\n')
320
+ .map((line) => line.trim())
321
+ .filter(Boolean)
322
+ .map((line) => line.replace(/,$/, ''))
323
+ .map((line) => {
324
+ const idMatch = line.match(/^([A-F0-9]{24})/);
325
+ return idMatch?.[1] ?? null;
326
+ })
327
+ .filter(Boolean);
328
+ }
329
+
330
+ function parsePbxprojObjectsByIsa(pbxprojContent, isa) {
331
+ const objectRegex = new RegExp(
332
+ `([A-F0-9]{24}) /\\* ([^*]+) \\*/ = \\{[\\s\\S]*?isa = ${isa};([\\s\\S]*?)\\n\\s*\\};`,
333
+ 'g'
334
+ );
335
+ const objects = [];
336
+ let match;
337
+
338
+ while ((match = objectRegex.exec(pbxprojContent)) !== null) {
339
+ objects.push({
340
+ id: match[1],
341
+ comment: match[2].trim(),
342
+ body: match[3],
343
+ });
344
+ }
345
+
346
+ return objects;
347
+ }
348
+
349
+ function isAppLikeTarget(targetName, productType) {
350
+ if (productType?.includes('application')) return true;
351
+
352
+ return ![
353
+ 'Tests',
354
+ 'UITests',
355
+ 'UnitTests',
356
+ 'NotificationService',
357
+ 'Extension',
358
+ 'Widget',
359
+ ].some((suffix) => targetName.endsWith(suffix));
360
+ }
361
+
362
+ function getAndroidBuildGradlePath(projectRoot) {
363
+ return findFirstExistingPath([
364
+ path.join(projectRoot, 'android', 'app', 'build.gradle'),
365
+ path.join(projectRoot, 'android', 'app', 'build.gradle.kts'),
366
+ ]);
367
+ }
368
+
369
+ function getAndroidProjectMetadata() {
370
+ const projectRoot = getProjectRoot();
371
+ const gradlePath = getAndroidBuildGradlePath(projectRoot);
372
+ if (!gradlePath) {
373
+ console.warn('⚠️ Android build.gradle not found.');
374
+ return null;
375
+ }
376
+
377
+ const gradleContent = fs.readFileSync(gradlePath, 'utf8');
378
+ const defaultConfigBlock = extractNamedBlock(gradleContent, 'defaultConfig');
379
+ const productFlavorsBlock = extractNamedBlock(
380
+ gradleContent,
381
+ 'productFlavors'
382
+ );
383
+
384
+ const defaultAppId =
385
+ readQuotedGradleValue(defaultConfigBlock?.content ?? '', 'applicationId') ??
386
+ null;
387
+ const defaultVersion =
388
+ readQuotedGradleValue(defaultConfigBlock?.content ?? '', 'versionName') ??
389
+ null;
390
+
391
+ const flavors = parseTopLevelNamedBlocks(
392
+ productFlavorsBlock?.content ?? ''
393
+ ).map(({ name, content }) => {
394
+ const flavorAppId = readQuotedGradleValue(content, 'applicationId');
395
+ const flavorAppIdSuffix = readQuotedGradleValue(
396
+ content,
397
+ 'applicationIdSuffix'
398
+ );
399
+ const flavorVersion = readQuotedGradleValue(content, 'versionName');
400
+ const flavorVersionSuffix = readQuotedGradleValue(
401
+ content,
402
+ 'versionNameSuffix'
403
+ );
404
+
405
+ return {
406
+ name,
407
+ label: name,
408
+ appId: flavorAppId ?? `${defaultAppId ?? ''}${flavorAppIdSuffix ?? ''}`,
409
+ version:
410
+ flavorVersion ??
411
+ (defaultVersion
412
+ ? `${defaultVersion}${flavorVersionSuffix ?? ''}`
413
+ : null),
414
+ };
415
+ });
416
+
417
+ return {
418
+ defaultConfig: {
419
+ name: 'default',
420
+ label: 'Default',
421
+ appId: defaultAppId,
422
+ version: defaultVersion,
423
+ },
424
+ flavors,
425
+ };
426
+ }
427
+
428
+ async function getAndroidFlavorSelection() {
429
+ const metadata = getAndroidProjectMetadata();
430
+ if (!metadata) return null;
233
431
 
234
- const pbxprojPath = path.join(iosDir, projectDir, 'project.pbxproj');
235
- if (!fs.existsSync(pbxprojPath)) {
236
- console.warn('⚠️ project.pbxproj not found.');
432
+ if (!metadata.flavors.length) {
433
+ return metadata.defaultConfig;
434
+ }
435
+
436
+ let selectedFlavor;
437
+ let isFlavorConfirmed = false;
438
+
439
+ while (!isFlavorConfirmed) {
440
+ selectedFlavor = await select({
441
+ message: 'Select Android flavor:',
442
+ choices: [
443
+ {
444
+ name: `Default (${metadata.defaultConfig.appId ?? 'unknown app id'} / ${metadata.defaultConfig.version ?? 'unknown version'})`,
445
+ value: metadata.defaultConfig,
446
+ },
447
+ ...metadata.flavors.map((flavor) => ({
448
+ name: `${flavor.name} (${flavor.appId ?? 'unknown app id'} / ${flavor.version ?? 'unknown version'})`,
449
+ value: flavor,
450
+ })),
451
+ ],
452
+ });
453
+
454
+ isFlavorConfirmed = await confirm({
455
+ message: `Continue with Android flavor ${selectedFlavor.label ?? selectedFlavor.name}?`,
456
+ default: true,
457
+ });
458
+ }
459
+
460
+ return selectedFlavor;
461
+ }
462
+
463
+ function getIosProjectFiles() {
464
+ const projectRoot = getProjectRoot();
465
+ const iosDir = path.join(projectRoot, 'ios');
466
+ if (!fs.existsSync(iosDir)) {
467
+ console.warn(`⚠️ iOS folder not found at ${iosDir}`);
468
+ return null;
469
+ }
470
+
471
+ const xcodeProjPath = findFirstXcodeProj(iosDir);
472
+ if (!xcodeProjPath) {
473
+ console.warn('⚠️ .xcodeproj not found inside ios directory.');
474
+ return null;
475
+ }
476
+
477
+ const pbxprojPath = path.join(xcodeProjPath, 'project.pbxproj');
478
+ if (!fs.existsSync(pbxprojPath)) {
479
+ console.warn('⚠️ project.pbxproj not found.');
480
+ return null;
481
+ }
482
+
483
+ return { iosDir, xcodeProjPath, pbxprojPath };
484
+ }
485
+
486
+ function getIosTargetMetadata() {
487
+ const projectFiles = getIosProjectFiles();
488
+ if (!projectFiles) return null;
489
+
490
+ const pbxprojContent = fs.readFileSync(projectFiles.pbxprojPath, 'utf8');
491
+ const configObjects = parsePbxprojObjectsByIsa(
492
+ pbxprojContent,
493
+ 'XCBuildConfiguration'
494
+ );
495
+ const configMap = new Map(
496
+ configObjects.map((config) => [
497
+ config.id,
498
+ {
499
+ name: cleanPbxString(readPbxValue(config.body, 'name')),
500
+ version: cleanPbxString(readPbxValue(config.body, 'MARKETING_VERSION')),
501
+ appId: cleanPbxString(
502
+ readPbxValue(config.body, 'PRODUCT_BUNDLE_IDENTIFIER')
503
+ ),
504
+ productName: cleanPbxString(readPbxValue(config.body, 'PRODUCT_NAME')),
505
+ },
506
+ ])
507
+ );
508
+
509
+ const configListObjects = parsePbxprojObjectsByIsa(
510
+ pbxprojContent,
511
+ 'XCConfigurationList'
512
+ );
513
+ const configListMap = new Map(
514
+ configListObjects.map((configList) => [
515
+ configList.id,
516
+ {
517
+ defaultName: cleanPbxString(
518
+ readPbxValue(configList.body, 'defaultConfigurationName')
519
+ ),
520
+ buildConfigurations: parsePbxArray(
521
+ configList.body,
522
+ 'buildConfigurations'
523
+ ),
524
+ },
525
+ ])
526
+ );
527
+
528
+ const targetObjects = parsePbxprojObjectsByIsa(
529
+ pbxprojContent,
530
+ 'PBXNativeTarget'
531
+ );
532
+ const targets = targetObjects
533
+ .map((target) => {
534
+ const targetName = cleanPbxString(readPbxValue(target.body, 'name'));
535
+ const productType = cleanPbxString(
536
+ readPbxValue(target.body, 'productType')
537
+ );
538
+ const configListId = cleanPbxString(
539
+ readPbxValue(target.body, 'buildConfigurationList')
540
+ )?.match(/^([A-F0-9]{24})/)?.[1];
541
+
542
+ if (
543
+ !targetName ||
544
+ !configListId ||
545
+ !isAppLikeTarget(targetName, productType)
546
+ ) {
237
547
  return null;
238
548
  }
239
549
 
240
- const pbxprojContent = fs.readFileSync(pbxprojPath, 'utf8');
241
- const match = pbxprojContent.match(/MARKETING_VERSION\s*=\s*([\d.]+);/);
550
+ const configList = configListMap.get(configListId);
551
+ const buildConfigs =
552
+ configList?.buildConfigurations
553
+ .map((configId) => configMap.get(configId))
554
+ .filter(Boolean) ?? [];
242
555
 
243
- if (match && match[1]) {
244
- return match[1];
245
- } else {
246
- console.warn('⚠️ Could not find MARKETING_VERSION in project.pbxproj.');
247
- }
248
- }
249
- } catch (err) {
250
- console.warn(`⚠️ Failed to read ${platform} version:`, err.message);
556
+ const configsByName = new Map(
557
+ buildConfigs
558
+ .filter((config) => config.name)
559
+ .map((config) => [config.name, config])
560
+ );
561
+
562
+ const preferredConfig =
563
+ buildConfigs.find(
564
+ (config) => config.name === configList?.defaultName
565
+ ) ??
566
+ buildConfigs.find((config) => config.name === 'Release') ??
567
+ buildConfigs[0];
568
+
569
+ if (!preferredConfig) return null;
570
+
571
+ return {
572
+ name: targetName,
573
+ label: targetName,
574
+ appId: preferredConfig.appId ?? null,
575
+ version: preferredConfig.version ?? null,
576
+ productName: preferredConfig.productName ?? targetName,
577
+ buildConfiguration: preferredConfig.name ?? null,
578
+ configsByName,
579
+ };
580
+ })
581
+ .filter(Boolean);
582
+
583
+ return {
584
+ projectFiles,
585
+ defaultConfig: targets[0] ?? null,
586
+ targets,
587
+ };
588
+ }
589
+
590
+ function parseSchemeFile(filePath) {
591
+ const content = fs.readFileSync(filePath, 'utf8');
592
+ const schemeName = path.basename(filePath, '.xcscheme');
593
+
594
+ const blueprintName =
595
+ content.match(/BlueprintName\s*=\s*"([^"]+)"/)?.[1] ?? schemeName;
596
+ const buildConfiguration =
597
+ content.match(
598
+ /ArchiveAction[^>]*buildConfiguration\s*=\s*"([^"]+)"/
599
+ )?.[1] ??
600
+ content.match(/LaunchAction[^>]*buildConfiguration\s*=\s*"([^"]+)"/)?.[1] ??
601
+ content.match(
602
+ /ProfileAction[^>]*buildConfiguration\s*=\s*"([^"]+)"/
603
+ )?.[1] ??
604
+ 'Release';
605
+
606
+ return {
607
+ scheme: schemeName,
608
+ targetName: blueprintName,
609
+ buildConfiguration,
610
+ };
611
+ }
612
+
613
+ function getIosSchemeMetadata() {
614
+ const iosMetadata = getIosTargetMetadata();
615
+ if (!iosMetadata) return null;
616
+
617
+ const schemeFiles = walkFiles(iosMetadata.projectFiles.iosDir, (filePath) =>
618
+ filePath.endsWith('.xcscheme')
619
+ );
620
+
621
+ const targetsByName = new Map(
622
+ iosMetadata.targets.map((target) => [target.name, target])
623
+ );
624
+
625
+ const schemes = schemeFiles
626
+ .map(parseSchemeFile)
627
+ .map((scheme) => {
628
+ const target = targetsByName.get(scheme.targetName);
629
+ if (!target) return null;
630
+
631
+ const config = target.configsByName.get(scheme.buildConfiguration) ??
632
+ target.configsByName.get('Release') ?? {
633
+ appId: target.appId,
634
+ version: target.version,
635
+ productName: target.productName,
636
+ name: target.buildConfiguration,
637
+ };
638
+
639
+ return {
640
+ name: scheme.scheme,
641
+ label: scheme.scheme,
642
+ targetName: target.name,
643
+ buildConfiguration: scheme.buildConfiguration,
644
+ appId: config.appId ?? target.appId ?? null,
645
+ version: config.version ?? target.version ?? null,
646
+ productName: config.productName ?? target.productName,
647
+ };
648
+ })
649
+ .filter(Boolean);
650
+
651
+ if (!schemes.length) {
652
+ return {
653
+ defaultConfig: iosMetadata.defaultConfig,
654
+ schemes: iosMetadata.targets.map((target) => ({
655
+ name: target.name,
656
+ label: target.label,
657
+ targetName: target.name,
658
+ buildConfiguration: target.buildConfiguration,
659
+ appId: target.appId,
660
+ version: target.version,
661
+ productName: target.productName,
662
+ })),
663
+ };
664
+ }
665
+
666
+ const uniqueSchemes = schemes.filter(
667
+ (scheme, index, allSchemes) =>
668
+ allSchemes.findIndex((candidate) => candidate.name === scheme.name) ===
669
+ index
670
+ );
671
+
672
+ return {
673
+ defaultConfig: uniqueSchemes[0],
674
+ schemes: uniqueSchemes,
675
+ };
676
+ }
677
+
678
+ async function getIosSchemeSelection() {
679
+ const metadata = getIosSchemeMetadata();
680
+ if (!metadata) return null;
681
+
682
+ if (metadata.schemes.length <= 1) {
683
+ return metadata.defaultConfig;
684
+ }
685
+
686
+ let selectedScheme;
687
+ let isSchemeConfirmed = false;
688
+
689
+ while (!isSchemeConfirmed) {
690
+ selectedScheme = await select({
691
+ message: 'Select iOS scheme:',
692
+ choices: metadata.schemes.map((scheme) => ({
693
+ name: `${scheme.label} (${scheme.appId ?? 'unknown app id'} / ${scheme.version ?? 'unknown version'})`,
694
+ value: scheme,
695
+ })),
696
+ });
697
+
698
+ isSchemeConfirmed = await confirm({
699
+ message: `Continue with iOS scheme ${selectedScheme.label ?? selectedScheme.name}?`,
700
+ default: true,
701
+ });
702
+ }
703
+
704
+ return selectedScheme;
705
+ }
706
+
707
+ function getPlatformAppVersion(platform, selection) {
708
+ if (selection?.version) return selection.version;
709
+
710
+ if (platform === 'android') {
711
+ const metadata = getAndroidProjectMetadata();
712
+ return metadata?.defaultConfig.version ?? null;
713
+ }
714
+
715
+ if (platform === 'ios') {
716
+ const metadata = getIosSchemeMetadata();
717
+ return metadata?.defaultConfig.version ?? null;
251
718
  }
252
719
 
253
720
  return null;
254
721
  }
255
722
 
256
- /**
257
- * Get common configuration (API token, project ID, env).
258
- */
259
723
  async function getCommonConfig() {
260
- console.log(`\n⚙️ Enter common configuration (applies to both platforms)\n`);
724
+ console.log(`\n⚙️ Enter common configuration for the app\n`);
261
725
 
262
726
  const API_TOKEN = await input({
263
- message: `Enter API Token:`,
727
+ message: 'Enter API Token:',
264
728
  validate: (val) => (val.trim() ? true : 'API Token is required'),
265
729
  });
266
730
 
267
731
  const PROJECT_ID = await input({
268
- message: `Enter Project ID:`,
732
+ message: 'Enter Project ID:',
269
733
  validate: (val) => (val.trim() ? true : 'Project ID is required'),
270
734
  });
271
735
 
272
- const ENVIRONMENT = await select({
273
- message: `Select Environment:`,
274
- choices: [
275
- { name: 'development', value: 'development' },
276
- { name: 'production', value: 'production' },
277
- ],
278
- });
736
+ let ENVIRONMENT;
737
+ let isEnvironmentConfirmed = false;
738
+
739
+ while (!isEnvironmentConfirmed) {
740
+ ENVIRONMENT = await select({
741
+ message: 'Select Environment:',
742
+ choices: [
743
+ { name: 'development', value: 'development' },
744
+ { name: 'production', value: 'production' },
745
+ ],
746
+ });
747
+
748
+ isEnvironmentConfirmed = await confirm({
749
+ message: `Continue with ${ENVIRONMENT} environment?`,
750
+ default: true,
751
+ });
752
+ }
279
753
 
280
754
  return { API_TOKEN, PROJECT_ID, ENVIRONMENT };
281
755
  }
282
756
 
283
- /**
284
- * Get platform-specific configuration with version auto-detection.
285
- */
286
- async function getPlatformSpecificConfig(platform) {
757
+ async function getPlatformConfig(platform) {
287
758
  console.log(`\n🔧 Configuring ${platform.toUpperCase()}...\n`);
288
759
 
289
- let detectedVersion = getAppVersion(platform);
760
+ const selection =
761
+ platform === 'android'
762
+ ? await getAndroidFlavorSelection()
763
+ : platform === 'ios'
764
+ ? await getIosSchemeSelection()
765
+ : null;
766
+
767
+ if (platform === 'android' && selection?.label) {
768
+ console.log(
769
+ `📦 Selected Android flavor: ${selection.label} (${selection.appId ?? 'unknown app id'})`
770
+ );
771
+ }
772
+
773
+ if (platform === 'ios' && selection?.label) {
774
+ console.log(
775
+ `🍎 Selected iOS scheme: ${selection.label} (${selection.appId ?? 'unknown app id'})`
776
+ );
777
+ }
778
+
779
+ let detectedVersion = getPlatformAppVersion(platform, selection);
290
780
  if (detectedVersion) {
291
781
  console.log(`📱 Detected ${platform} version: ${detectedVersion}`);
292
782
  } else {
@@ -302,12 +792,106 @@ async function getPlatformSpecificConfig(platform) {
302
792
  default: false,
303
793
  });
304
794
 
305
- return { VERSION: detectedVersion, FORCE_UPDATE };
795
+ return {
796
+ VERSION: detectedVersion,
797
+ FORCE_UPDATE,
798
+ APP_ID: selection?.appId ?? null,
799
+ VARIANT: selection?.name ?? null,
800
+ };
801
+ }
802
+
803
+ function getAndroidOutputPaths(projectRoot) {
804
+ return {
805
+ bundleZipPath: path.join(
806
+ projectRoot,
807
+ 'android',
808
+ 'index.android.bundle.zip'
809
+ ),
810
+ outputDir: path.join(projectRoot, 'android', 'output'),
811
+ sourceMapPath: path.join(projectRoot, 'android', 'sourcemap.js'),
812
+ sourceMapZipPath: path.join(projectRoot, 'android', 'sourcemap.zip'),
813
+ };
814
+ }
815
+
816
+ function getIosOutputPaths(projectRoot) {
817
+ return {
818
+ bundleZipPath: path.join(projectRoot, 'ios', 'main.jsbundle.zip'),
819
+ outputDir: path.join(projectRoot, 'ios', 'output'),
820
+ sourceMapPath: path.join(projectRoot, 'ios', 'sourcemap.js'),
821
+ sourceMapZipPath: path.join(projectRoot, 'ios', 'sourcemap.zip'),
822
+ };
823
+ }
824
+
825
+ function buildAndroid(selection) {
826
+ const projectRoot = getProjectRoot();
827
+ const paths = getAndroidOutputPaths(projectRoot);
828
+
829
+ console.log('📦 Building Android bundle...');
830
+ if (selection?.label) {
831
+ console.log(`📦 Using Android flavor: ${selection.label}`);
832
+ }
833
+
834
+ fs.rmSync(paths.outputDir, { recursive: true, force: true });
835
+ fs.rmSync(paths.bundleZipPath, { force: true });
836
+ fs.rmSync(paths.sourceMapPath, { force: true });
837
+ fs.rmSync(paths.sourceMapZipPath, { force: true });
838
+ fs.mkdirSync(paths.outputDir, { recursive: true });
839
+
840
+ run(
841
+ `react-native bundle --platform android --dev false --entry-file index.js ` +
842
+ `--bundle-output ${path.relative(projectRoot, path.join(paths.outputDir, 'index.android.bundle'))} ` +
843
+ `--assets-dest ${path.relative(projectRoot, paths.outputDir)} ` +
844
+ `--sourcemap-output ${path.relative(projectRoot, paths.sourceMapPath)}`,
845
+ projectRoot
846
+ );
847
+ run(
848
+ `find output -type f | zip index.android.bundle.zip -@`,
849
+ path.join(projectRoot, 'android')
850
+ );
851
+ run(`zip sourcemap.zip sourcemap.js`, path.join(projectRoot, 'android'));
852
+
853
+ fs.rmSync(paths.outputDir, { recursive: true, force: true });
854
+ fs.rmSync(paths.sourceMapPath, { force: true });
855
+
856
+ console.log(`✅ Android bundle created at ${paths.bundleZipPath}`);
857
+ return paths.bundleZipPath;
858
+ }
859
+
860
+ function buildIOS(selection) {
861
+ const projectRoot = getProjectRoot();
862
+ const paths = getIosOutputPaths(projectRoot);
863
+
864
+ console.log('📦 Building iOS bundle...');
865
+ if (selection?.label) {
866
+ console.log(`🍎 Using iOS scheme: ${selection.label}`);
867
+ }
868
+
869
+ fs.rmSync(paths.outputDir, { recursive: true, force: true });
870
+ fs.rmSync(paths.bundleZipPath, { force: true });
871
+ fs.rmSync(paths.sourceMapPath, { force: true });
872
+ fs.rmSync(paths.sourceMapZipPath, { force: true });
873
+ fs.mkdirSync(paths.outputDir, { recursive: true });
874
+
875
+ run(
876
+ `react-native bundle --platform ios --dev false --entry-file index.js ` +
877
+ `--bundle-output ${path.relative(projectRoot, path.join(paths.outputDir, 'main.jsbundle'))} ` +
878
+ `--assets-dest ${path.relative(projectRoot, paths.outputDir)} ` +
879
+ `--sourcemap-output ${path.relative(projectRoot, paths.sourceMapPath)}`,
880
+ projectRoot
881
+ );
882
+ run(
883
+ `find output -type f | zip main.jsbundle.zip -@`,
884
+ path.join(projectRoot, 'ios')
885
+ );
886
+ run(`zip sourcemap.zip sourcemap.js`, path.join(projectRoot, 'ios'));
887
+
888
+ fs.rmSync(paths.outputDir, { recursive: true, force: true });
889
+ fs.rmSync(paths.sourceMapPath, { force: true });
890
+
891
+ console.log(`✅ iOS bundle created at ${paths.bundleZipPath}`);
892
+ return paths.bundleZipPath;
306
893
  }
307
894
 
308
- /**
309
- * Entry point
310
- */
311
895
  (async () => {
312
896
  try {
313
897
  const platform = process.argv[2];
@@ -319,34 +903,36 @@ async function getPlatformSpecificConfig(platform) {
319
903
  const commonConfig = await getCommonConfig();
320
904
 
321
905
  if (platform === 'android') {
322
- const androidExtra = await getPlatformSpecificConfig('android');
323
- const config = { ...commonConfig, ...androidExtra };
324
- const androidFile = buildAndroid();
906
+ const androidConfig = await getPlatformConfig('android');
907
+ const androidFile = buildAndroid({ label: androidConfig.VARIANT });
325
908
  await uploadBundle({
326
909
  filePath: androidFile,
327
910
  platform: 'android',
328
- config,
911
+ config: { ...commonConfig, ...androidConfig },
329
912
  });
330
913
  } else if (platform === 'ios') {
331
- const iosExtra = await getPlatformSpecificConfig('ios');
332
- const config = { ...commonConfig, ...iosExtra };
333
- const iosFile = buildIOS();
334
- await uploadBundle({ filePath: iosFile, platform: 'ios', config });
914
+ const iosConfig = await getPlatformConfig('ios');
915
+ const iosFile = buildIOS({ label: iosConfig.VARIANT });
916
+ await uploadBundle({
917
+ filePath: iosFile,
918
+ platform: 'ios',
919
+ config: { ...commonConfig, ...iosConfig },
920
+ });
335
921
  } else if (platform === 'all') {
336
- const androidExtra = await getPlatformSpecificConfig('android');
337
- const androidFile = buildAndroid();
922
+ const androidConfig = await getPlatformConfig('android');
923
+ const androidFile = buildAndroid({ label: androidConfig.VARIANT });
338
924
  await uploadBundle({
339
925
  filePath: androidFile,
340
926
  platform: 'android',
341
- config: { ...commonConfig, ...androidExtra },
927
+ config: { ...commonConfig, ...androidConfig },
342
928
  });
343
929
 
344
- const iosExtra = await getPlatformSpecificConfig('ios');
345
- const iosFile = buildIOS();
930
+ const iosConfig = await getPlatformConfig('ios');
931
+ const iosFile = buildIOS({ label: iosConfig.VARIANT });
346
932
  await uploadBundle({
347
933
  filePath: iosFile,
348
934
  platform: 'ios',
349
- config: { ...commonConfig, ...iosExtra },
935
+ config: { ...commonConfig, ...iosConfig },
350
936
  });
351
937
  } else {
352
938
  console.log('❌ Invalid option. Use: android | ios | all');