feat: implement modification of the Xcodeproject

This commit is contained in:
Riccardo Cipolleschi
2025-08-15 18:20:31 +01:00
parent b1a22a32ad
commit 22a7cf4973
2 changed files with 214 additions and 5 deletions
+52 -5
View File
@@ -22,6 +22,7 @@ const {execSync} = require('child_process');
// Import functions from other scripts
const {prepareAppDependenciesHeaders} = require('./prepare-app-dependencies-headers');
const {createSymlinks: createSymlinksFunction} = require('./create-symlinks');
const {integrateSwiftPackagesInXcode} = require('./update-xcodeproject');
const codegenExecutor = require('../codegen/generate-artifacts-executor');
/**
@@ -29,8 +30,9 @@ const codegenExecutor = require('../codegen/generate-artifacts-executor');
* @param {string} appPath - Path to the app (e.g., '../../private/helloworld')
* @param {string} reactNativePath - Path to the React Native package (e.g., '.')
* @param {string} appXcodeProject - Name of the Xcode project (e.g., 'HelloWorld.xcodeproj')
* @param {string} targetName - Name of the app target (e.g., 'HelloWorld')
*/
async function prepareApp(appPath = '../../private/helloworld', reactNativePath = '.', appXcodeProject = 'HelloWorld.xcodeproj') {
async function prepareApp(appPath = '../../private/helloworld', reactNativePath = '.', appXcodeProject = 'HelloWorld.xcodeproj', targetName = 'HelloWorld') {
console.log('🚀 Starting app preparation for SwiftPM build from source...');
// Resolve absolute paths
@@ -80,8 +82,12 @@ async function prepareApp(appPath = '../../private/helloworld', reactNativePath
console.log('\n🔧 Step 7: Fixing REACT_NATIVE_PATH in Xcode project...');
await fixReactNativePath(appIosPath, absoluteReactNativePath, appXcodeProject);
// Step 8: Open Xcode project
console.log('\n📱 Step 8: Opening Xcode project...');
// Step 8: Integrate SwiftPM packages in Xcode
console.log('\n📦 Step 8: Integrating SwiftPM packages in Xcode...');
await integrateSwiftPMPackages(appIosPath, absoluteReactNativePath, absoluteAppPath, appXcodeProject, targetName);
// Step 9: Open Xcode project
console.log('\n📱 Step 9: Opening Xcode project...');
await openXcodeProject(appIosPath, appXcodeProject);
console.log('\n✅ App preparation completed successfully!');
@@ -268,6 +274,40 @@ async function fixReactNativePath(appIosPath, reactNativePath, appXcodeProject)
}
}
/**
* Integrate SwiftPM packages into Xcode project
*/
async function integrateSwiftPMPackages(appIosPath, reactNativePath, appPath, appXcodeProject, targetName) {
try {
console.log('Preparing SwiftPM package integrations...');
// Calculate relative paths from appIosPath
const relativeReactNativePath = path.relative(appIosPath, reactNativePath);
const relativeGeneratedPath = path.join('build', 'generated', 'ios');
// Create PackageSwift objects
const packageSwiftObjects = [
{
"relativePath": relativeReactNativePath,
"targets": ["React"]
},
{
"relativePath": relativeGeneratedPath,
"targets": ["ReactCodegen", "ReactAppDependencyProvider"]
}
];
const xcodeProjectPath = path.join(appIosPath, appXcodeProject);
// Call integrateSwiftPackagesInXcode function
integrateSwiftPackagesInXcode(xcodeProjectPath, packageSwiftObjects, targetName);
console.log('✓ SwiftPM packages integrated into Xcode project');
} catch (error) {
throw new Error(`SwiftPM integration failed: ${error.message}`);
}
}
/**
* Open Xcode project
*/
@@ -296,6 +336,7 @@ if (require.main === module) {
let appPath = '../../private/helloworld';
let reactNativePath = '.';
let appXcodeProject = 'HelloWorld.xcodeproj';
let targetName = 'HelloWorld';
if (args.length >= 1) {
appPath = args[0];
@@ -309,12 +350,17 @@ if (require.main === module) {
appXcodeProject = args[2];
}
console.log('Usage: node prepare-app.js [appPath] [reactNativePath] [appXcodeProject]');
if (args.length >= 4) {
targetName = args[3];
}
console.log('Usage: node prepare-app.js [appPath] [reactNativePath] [appXcodeProject] [targetName]');
console.log(`Using App path: ${appPath}`);
console.log(`Using React Native path: ${reactNativePath}`);
console.log(`Using App Xcode project: ${appXcodeProject}`);
console.log(`Using Target name: ${targetName}`);
prepareApp(appPath, reactNativePath, appXcodeProject)
prepareApp(appPath, reactNativePath, appXcodeProject, targetName)
.then(() => {
console.log('\n🎉 All done! Your app is ready for SwiftPM build from source.');
process.exit(0);
@@ -334,5 +380,6 @@ module.exports = {
generateCodegenArtifacts,
prepareHeaders,
fixReactNativePath,
integrateSwiftPMPackages,
openXcodeProject
};
@@ -0,0 +1,162 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
const crypto = require('crypto');
const {execSync} = require('child_process');
/**
* Generate a random string of 24 HEX characters (capital letters) for Xcode object IDs
* @returns {string} A 24-character hexadecimal string in uppercase
*/
function generateXcodeObjectId() {
return crypto.randomBytes(12).toString('hex').toUpperCase();
}
/**
* Convert Xcode project.pbxproj file to JSON format
* @param {string} projectPath - Path to the project.pbxproj file
* @returns {Object} Parsed JSON object of the Xcode project
*/
function convertXcodeProjectToJSON(projectPath) {
const command = `plutil -convert json -o - "${projectPath}"`;
const jsonOutput = execSync(command, { encoding: 'utf8' });
return JSON.parse(jsonOutput);
}
/**
* Add local SwiftPM package references and product dependencies to Xcode project
* @param {string} relativePath - The relative path of where the Package.swift is located
* @param {Array<string>} productNames - List of product names exposed by the Package.swift files
* @param {Object} xcodeProject - The xcode project converted in JSON format
* @param {string} targetName - The name of the target to add dependencies to
*/
function addLocalSwiftPM(relativePath, productNames, xcodeProject, targetName) {
// For the relative path: create XCLocalSwiftPackageReference
const packageReferenceId = generateXcodeObjectId();
xcodeProject.objects[packageReferenceId] = {
"isa": "XCLocalSwiftPackageReference",
"relativePath": relativePath
};
// Find PBXProject object and update packageReferences
const objects = xcodeProject.objects;
for (const objectId in objects) {
const object = objects[objectId];
if (object.isa === "PBXProject") {
if (!object.packageReferences) {
object.packageReferences = [];
}
object.packageReferences.push(packageReferenceId);
break;
}
}
// For each product: create XCSwiftPackageProductDependency and PBXBuildFile
for (const productName of productNames) {
// Generate XcodeID for XCSwiftPackageProductDependency
const productDependencyId = generateXcodeObjectId();
xcodeProject.objects[productDependencyId] = {
"isa": "XCSwiftPackageProductDependency",
"productName": productName
};
// Generate second XcodeID for PBXBuildFile
const buildFileId = generateXcodeObjectId();
xcodeProject.objects[buildFileId] = {
"isa": "PBXBuildFile",
"productRef": productDependencyId
};
// Find PBXNativeTarget with matching name
for (const objectId in objects) {
const object = objects[objectId];
if (object.isa === "PBXNativeTarget" && object.name === targetName) {
// Iterate over buildPhases to find PBXFrameworksBuildPhase
for (const buildPhaseId of object.buildPhases) {
const buildPhaseObject = objects[buildPhaseId];
if (buildPhaseObject && buildPhaseObject.isa === "PBXFrameworksBuildPhase") {
// Add buildFileId to the files array
if (!buildPhaseObject.files) {
buildPhaseObject.files = [];
}
buildPhaseObject.files.push(buildFileId);
break;
}
}
break;
}
}
}
}
/**
* Integrate Swift packages into Xcode project
* @param {string} xcodeProjectPath - Path to the app.xcodeproj file
* @param {Array<Object>} packageSwiftObjects - List of PackageSwift objects with relativePath and targets
* @param {string} appTargetName - Name of the app target
*/
function integrateSwiftPackagesInXcode(xcodeProjectPath, packageSwiftObjects, appTargetName) {
const fs = require('fs');
const path = require('path');
// Construct path to project.pbxproj
const projectPbxprojPath = path.join(xcodeProjectPath, 'project.pbxproj');
if (!fs.existsSync(projectPbxprojPath)) {
throw new Error(`Project file not found: ${projectPbxprojPath}`);
}
// Convert to JSON
const xcodeProject = convertXcodeProjectToJSON(projectPbxprojPath);
// Iterate over PackageSwift objects and execute addLocalSwiftPM
for (const packageSwift of packageSwiftObjects) {
addLocalSwiftPM(
packageSwift.relativePath,
packageSwift.targets,
xcodeProject,
appTargetName
);
}
// Write JSON directly to the project.pbxproj file
fs.writeFileSync(projectPbxprojPath, JSON.stringify(xcodeProject));
}
// CLI usage
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 3) {
console.log('Usage: node update-xcodeproject.js <xcodeProjectPath> <appTargetName> <packageSwiftObjectsJSON>');
console.log('Example: node update-xcodeproject.js ./MyApp.xcodeproj MyApp \'[{"relativePath":"../react-native","targets":["ReactCommon","React-Core"]}]\'');
process.exit(1);
}
const xcodeProjectPath = args[0];
const appTargetName = args[1];
const packageSwiftObjectsJSON = args[2];
try {
const packageSwiftObjects = JSON.parse(packageSwiftObjectsJSON);
integrateSwiftPackagesInXcode(xcodeProjectPath, packageSwiftObjects, appTargetName);
console.log('✅ Successfully integrated Swift packages into Xcode project');
} catch (error) {
console.error('❌ Error:', error.message);
process.exit(1);
}
}
module.exports = {
generateXcodeObjectId,
convertXcodeProjectToJSON,
addLocalSwiftPM,
integrateSwiftPackagesInXcode
};