react-native-3rddigital-appupdate 1.0.18 → 1.0.20

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 +250 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-3rddigital-appupdate",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
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
@@ -169,6 +169,54 @@ function findFirstXcodeProj(dir) {
169
169
  return null;
170
170
  }
171
171
 
172
+ function findFirstXcworkspace(dir) {
173
+ const files = fs.readdirSync(dir);
174
+
175
+ for (const file of files) {
176
+ const fullPath = path.join(dir, file);
177
+ const stat = fs.statSync(fullPath);
178
+
179
+ if (stat.isDirectory()) {
180
+ if (file.endsWith('.xcworkspace') && file !== 'Pods.xcworkspace') {
181
+ return fullPath;
182
+ }
183
+ const nested = findFirstXcworkspace(fullPath);
184
+ if (nested) return nested;
185
+ }
186
+ }
187
+
188
+ return null;
189
+ }
190
+
191
+ function quoteShellArg(value) {
192
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
193
+ }
194
+
195
+ function runAndCapture(command, cwd = process.cwd()) {
196
+ try {
197
+ return execSync(command, {
198
+ cwd,
199
+ stdio: ['ignore', 'pipe', 'pipe'],
200
+ })
201
+ .toString()
202
+ .trim();
203
+ } catch (_error) {
204
+ return null;
205
+ }
206
+ }
207
+
208
+ function extractJsonObject(text) {
209
+ if (!text) return null;
210
+
211
+ const start = text.indexOf('{');
212
+ const end = text.lastIndexOf('}');
213
+ if (start === -1 || end === -1 || end <= start) {
214
+ return null;
215
+ }
216
+
217
+ return text.slice(start, end + 1);
218
+ }
219
+
172
220
  function extractBracedBlock(content, startIndex) {
173
221
  const openIndex = content.indexOf('{', startIndex);
174
222
  if (openIndex === -1) return null;
@@ -349,6 +397,14 @@ function choosePreferredBuildConfig(buildConfigs, preferredNames = []) {
349
397
  return buildConfigs[0] ?? null;
350
398
  }
351
399
 
400
+ function buildIosEntryLabel({ targetName, buildConfiguration, appId }) {
401
+ const buildConfigurationLabel = buildConfiguration
402
+ ? ` [${buildConfiguration}]`
403
+ : '';
404
+ const appIdLabel = appId ? ` (${appId})` : '';
405
+ return `${targetName}${buildConfigurationLabel}${appIdLabel}`;
406
+ }
407
+
352
408
  function getAndroidBuildGradlePath(projectRoot) {
353
409
  return findFirstExistingPath([
354
410
  path.join(projectRoot, 'android', 'app', 'build.gradle'),
@@ -464,19 +520,134 @@ function getIosProjectFiles() {
464
520
  return null;
465
521
  }
466
522
 
523
+ const xcworkspacePath = findFirstXcworkspace(iosDir);
524
+
467
525
  const pbxprojPath = path.join(xcodeProjPath, 'project.pbxproj');
468
526
  if (!fs.existsSync(pbxprojPath)) {
469
527
  console.warn('⚠️ project.pbxproj not found.');
470
528
  return null;
471
529
  }
472
530
 
473
- return { iosDir, xcodeProjPath, pbxprojPath };
531
+ return { iosDir, xcodeProjPath, xcworkspacePath, pbxprojPath };
532
+ }
533
+
534
+ function getIosBuildContainerArgs(projectFiles) {
535
+ if (projectFiles.xcworkspacePath) {
536
+ return `-workspace ${quoteShellArg(projectFiles.xcworkspacePath)}`;
537
+ }
538
+
539
+ return `-project ${quoteShellArg(projectFiles.xcodeProjPath)}`;
540
+ }
541
+
542
+ function parseXcodebuildSettings(output) {
543
+ const settings = {};
544
+
545
+ for (const line of output.split('\n')) {
546
+ const match = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.+)$/);
547
+ if (!match) continue;
548
+ settings[match[1]] = cleanPbxString(match[2]);
549
+ }
550
+
551
+ return settings;
552
+ }
553
+
554
+ function isResolvedBuildSetting(value) {
555
+ if (!value) return false;
556
+
557
+ return !/[()$]/.test(value);
558
+ }
559
+
560
+ function getIosSchemeMetadataFromXcodebuild(projectFiles) {
561
+ const projectRoot = getProjectRoot();
562
+ const buildContainerArgs = getIosBuildContainerArgs(projectFiles);
563
+ const listOutput = runAndCapture(
564
+ `xcodebuild -list -json ${buildContainerArgs}`,
565
+ projectRoot
566
+ );
567
+
568
+ if (!listOutput) return null;
569
+
570
+ try {
571
+ const jsonOutput = extractJsonObject(listOutput);
572
+ if (!jsonOutput) return null;
573
+
574
+ const parsed = JSON.parse(jsonOutput);
575
+ const schemes =
576
+ parsed.project?.schemes ??
577
+ parsed.workspace?.schemes ??
578
+ parsed.project?.targets ??
579
+ [];
580
+
581
+ const uniqueSchemes = [...new Set(schemes)].filter(Boolean);
582
+ if (!uniqueSchemes.length) return null;
583
+
584
+ const entries = uniqueSchemes
585
+ .map((schemeName) => {
586
+ const buildConfigurations = ['Release', 'Profile', 'Debug'];
587
+ const buildSettingsOutput = buildConfigurations
588
+ .map((configuration) => ({
589
+ configuration,
590
+ output: runAndCapture(
591
+ `xcodebuild -showBuildSettings ${buildContainerArgs} -scheme ${quoteShellArg(
592
+ schemeName
593
+ )} -configuration ${quoteShellArg(configuration)}`,
594
+ projectRoot
595
+ ),
596
+ }))
597
+ .find((item) => item.output)?.output;
598
+
599
+ if (!buildSettingsOutput) return null;
600
+
601
+ const settings = parseXcodebuildSettings(buildSettingsOutput);
602
+ const appId = isResolvedBuildSetting(settings.PRODUCT_BUNDLE_IDENTIFIER)
603
+ ? settings.PRODUCT_BUNDLE_IDENTIFIER
604
+ : null;
605
+ const version = isResolvedBuildSetting(settings.MARKETING_VERSION)
606
+ ? settings.MARKETING_VERSION
607
+ : null;
608
+ const targetName =
609
+ settings.TARGET_NAME || settings.PRODUCT_NAME || schemeName;
610
+
611
+ return {
612
+ name: schemeName,
613
+ targetName,
614
+ label: schemeName,
615
+ appId,
616
+ version,
617
+ productName: settings.PRODUCT_NAME || targetName,
618
+ buildConfiguration: settings.CONFIGURATION || 'Release',
619
+ };
620
+ })
621
+ .filter(Boolean);
622
+
623
+ if (!entries.length) return null;
624
+
625
+ const dedupedEntries = entries.filter(
626
+ (entry, index, allEntries) =>
627
+ allEntries.findIndex(
628
+ (candidate) =>
629
+ candidate.name === entry.name && candidate.appId === entry.appId
630
+ ) === index
631
+ );
632
+
633
+ return {
634
+ defaultConfig: dedupedEntries[0] ?? null,
635
+ targets: dedupedEntries,
636
+ };
637
+ } catch (_error) {
638
+ return null;
639
+ }
474
640
  }
475
641
 
476
642
  function getIosTargetMetadata() {
477
643
  const projectFiles = getIosProjectFiles();
478
644
  if (!projectFiles) return null;
479
645
 
646
+ const xcodebuildMetadata = getIosSchemeMetadataFromXcodebuild(projectFiles);
647
+ if (xcodebuildMetadata?.targets?.length) {
648
+ return xcodebuildMetadata;
649
+ }
650
+
480
651
  const pbxprojContent = fs.readFileSync(projectFiles.pbxprojPath, 'utf8');
481
652
  const configObjects = parsePbxprojObjectsByIsa(
482
653
  pbxprojContent,
@@ -516,6 +687,18 @@ function getIosTargetMetadata() {
516
687
  ])
517
688
  );
518
689
 
690
+ const preferredConfigNames = ['Release', 'Profile', 'Debug'];
691
+ const fallbackConfigGroups = new Map();
692
+
693
+ for (const config of configMap.values()) {
694
+ const appIdKey = config.appId ?? `no-app-id::${config.id}`;
695
+ if (!fallbackConfigGroups.has(appIdKey)) {
696
+ fallbackConfigGroups.set(appIdKey, []);
697
+ }
698
+
699
+ fallbackConfigGroups.get(appIdKey).push(config);
700
+ }
701
+
519
702
  const targetObjects = parsePbxprojObjectsByIsa(
520
703
  pbxprojContent,
521
704
  'PBXNativeTarget'
@@ -560,7 +743,7 @@ function getIosTargetMetadata() {
560
743
  configsByAppId.get(appIdKey).push(buildConfig);
561
744
  }
562
745
 
563
- const preferredConfigNames = [
746
+ const targetPreferredConfigNames = [
564
747
  configList?.defaultName,
565
748
  'Release',
566
749
  'Profile',
@@ -574,28 +757,27 @@ function getIosTargetMetadata() {
574
757
  buildConfigsWithAppId.length
575
758
  ? buildConfigsWithAppId
576
759
  : buildConfigs,
577
- preferredConfigNames
760
+ targetPreferredConfigNames
578
761
  ),
579
762
  ].filter(Boolean)
580
763
  : Array.from(configsByAppId.values())
581
764
  .map((configGroup) =>
582
- choosePreferredBuildConfig(configGroup, preferredConfigNames)
765
+ choosePreferredBuildConfig(
766
+ configGroup,
767
+ targetPreferredConfigNames
768
+ )
583
769
  )
584
770
  .filter(Boolean);
585
771
 
586
772
  return distinctConfigs.map((selectedConfig) => {
587
- const buildConfigurationLabel = selectedConfig.name
588
- ? ` [${selectedConfig.name}]`
589
- : '';
590
-
591
- const appIdLabel = selectedConfig.appId
592
- ? ` (${selectedConfig.appId})`
593
- : '';
594
-
595
773
  return {
596
774
  name: `${targetName}::${selectedConfig.appId ?? selectedConfig.id ?? 'no-app-id'}`,
597
775
  targetName,
598
- label: `${targetName}${buildConfigurationLabel}${appIdLabel}`,
776
+ label: buildIosEntryLabel({
777
+ targetName,
778
+ buildConfiguration: selectedConfig.name ?? null,
779
+ appId: selectedConfig.appId ?? null,
780
+ }),
599
781
  appId: selectedConfig.appId ?? null,
600
782
  version: selectedConfig.version ?? null,
601
783
  productName: selectedConfig.productName ?? targetName,
@@ -616,9 +798,62 @@ function getIosTargetMetadata() {
616
798
  ) === index
617
799
  );
618
800
 
801
+ const existingAppIds = new Set(
802
+ uniqueTargets.map((target) => target.appId).filter(Boolean)
803
+ );
804
+
805
+ const fallbackEntries = Array.from(fallbackConfigGroups.entries())
806
+ .filter(([appId]) => !existingAppIds.has(appId))
807
+ .map(([appId, configs]) => {
808
+ const selectedConfig = choosePreferredBuildConfig(
809
+ configs,
810
+ preferredConfigNames
811
+ );
812
+ if (!selectedConfig) return null;
813
+
814
+ const targetName =
815
+ selectedConfig.productName &&
816
+ selectedConfig.productName !== '$(TARGET_NAME)'
817
+ ? selectedConfig.productName
818
+ : 'Default';
819
+
820
+ return {
821
+ name: `${targetName}::${appId}`,
822
+ targetName,
823
+ label: buildIosEntryLabel({
824
+ targetName,
825
+ buildConfiguration: selectedConfig.name ?? null,
826
+ appId: selectedConfig.appId ?? null,
827
+ }),
828
+ appId: selectedConfig.appId ?? null,
829
+ version: selectedConfig.version ?? null,
830
+ productName: selectedConfig.productName ?? targetName,
831
+ buildConfiguration: selectedConfig.name ?? null,
832
+ };
833
+ })
834
+ .filter(Boolean);
835
+
836
+ const mergedTargets = [...uniqueTargets, ...fallbackEntries];
837
+
838
+ if (!mergedTargets.length) {
839
+ const fallbackVersionMatch = pbxprojContent.match(
840
+ /MARKETING_VERSION\s*=\s*([^;]+);/
841
+ );
842
+
843
+ return {
844
+ defaultConfig: {
845
+ name: 'default',
846
+ label: 'Default',
847
+ appId: null,
848
+ version: cleanPbxString(fallbackVersionMatch?.[1]) ?? null,
849
+ },
850
+ targets: [],
851
+ };
852
+ }
853
+
619
854
  return {
620
- defaultConfig: uniqueTargets[0] ?? null,
621
- targets: uniqueTargets,
855
+ defaultConfig: mergedTargets[0] ?? null,
856
+ targets: mergedTargets,
622
857
  };
623
858
  }
624
859