#!/usr/bin/env node /** * Post-prebuild script: adds SentrySPM Swift Package to the watch target. * * Run after `expo prebuild`: * node scripts/add-sentry-watch.js */ const fs = require("fs"); const path = require("path"); const SENTRY_REPO_URL = "https://github.com/getsentry/sentry-cocoa.git"; const SENTRY_MIN_VERSION = "9.25.0"; const SENTRY_PRODUCT = "Sentry"; function genUuid() { const hex = "0123456789ABCDEF"; let id = ""; for (let i = 0; i < 24; i++) id += hex[Math.floor(Math.random() * 16)]; return id; } const projectRoot = path.resolve(__dirname, ".."); const pbxprojPath = path.join( projectRoot, "ios", "SwimBuddy.xcodeproj", "project.pbxproj" ); if (!fs.existsSync(pbxprojPath)) { console.error("project.pbxproj not found. Run expo prebuild first."); process.exit(1); } let content = fs.readFileSync(pbxprojPath, "utf-8"); // Already added? if (content.includes(SENTRY_REPO_URL)) { console.log("SentrySPM already present."); process.exit(0); } // ── Find watch target UUID ── const watchTargetMatch = content.match( /(\w{24})\s*\/\* SwimBuddy Watch \*\/\s*=\s*\{\s*\n\s*isa = PBXNativeTarget;\s*\n\s*buildConfigurationList = \w+ / ); if (!watchTargetMatch) { console.error("Watch target not found"); process.exit(1); } const watchTargetUuid = watchTargetMatch[1]; console.log(`Watch target: ${watchTargetUuid}`); // ── Find watch target's Frameworks build phase UUID ── const watchTargetBlock = content.match( new RegExp( `${watchTargetUuid}\\s*\\/\\* SwimBuddy Watch \\*\\/\\s*=\\s*\\{[\\s\\S]*?buildPhases\\s*=\\s*\\(([\\s\\S]*?)\\);` ) ); if (!watchTargetBlock) { console.error("Cannot parse watch target buildPhases"); process.exit(1); } const fwPhaseMatch = watchTargetBlock[1].match(/(\w{24})\s*\/\* Frameworks \*\//); if (!fwPhaseMatch) { console.error("Watch Frameworks phase not found"); process.exit(1); } const watchFwPhaseId = fwPhaseMatch[1]; console.log(`Watch Frameworks phase: ${watchFwPhaseId}`); // ── Generate UUIDs ── const pkgRefId = genUuid(); const prodDepId = genUuid(); const buildFileId = genUuid(); // ── 1. PBXBuildFile (single-line format) ── const buildFileLine = `\t\t${buildFileId} /* ${SENTRY_PRODUCT} in Frameworks */ = {isa = PBXBuildFile; productRef = ${prodDepId} /* ${SENTRY_PRODUCT} */; };`; content = content.replace( /\/\* End PBXBuildFile section \*\//, `${buildFileLine}\n/* End PBXBuildFile section */` ); // ── 2. XCRemoteSwiftPackageReference section ── const pkgRefLine = `\t\t${pkgRefId} /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {isa = XCRemoteSwiftPackageReference; repositoryURL = "${SENTRY_REPO_URL}"; requirement = {kind = "upToNextMajorVersion"; minimumVersion = "${SENTRY_MIN_VERSION}"; }; };`; if (!content.includes("/* Begin XCRemoteSwiftPackageReference section */")) { content = content.replace( /\/\* End PBXProject section \*\//, `/* End PBXProject section */\n/* Begin XCRemoteSwiftPackageReference section */\n${pkgRefLine}\n/* End XCRemoteSwiftPackageReference section */` ); } else { content = content.replace( /\/\* End XCRemoteSwiftPackageReference section \*\//, `${pkgRefLine}\n/* End XCRemoteSwiftPackageReference section */` ); } // ── 3. XCSwiftPackageProductDependency section ── const prodDepLine = `\t\t${prodDepId} /* ${SENTRY_PRODUCT} */ = {isa = XCSwiftPackageProductDependency; package = ${pkgRefId} /* XCRemoteSwiftPackageReference "sentry-cocoa" */; productName = ${SENTRY_PRODUCT}; };`; if (!content.includes("/* Begin XCSwiftPackageProductDependency section */")) { content = content.replace( /\/\* End XCRemoteSwiftPackageReference section \*\//, `/* End XCRemoteSwiftPackageReference section */\n/* Begin XCSwiftPackageProductDependency section */\n${prodDepLine}\n/* End XCSwiftPackageProductDependency section */` ); } else { content = content.replace( /\/\* End XCSwiftPackageProductDependency section \*\//, `${prodDepLine}\n/* End XCSwiftPackageProductDependency section */` ); } // ── 4. Add to watch Frameworks build phase ── const fwPhasePattern = new RegExp( `(${watchFwPhaseId}\\s*\\/\\* Frameworks \\*\\/ = \\{[\\s\\S]*?files = \\()[\\s\\S]*?(\\);[\\s\\S]*?runOnlyForDeploymentPostprocessing)` ); const fwMatch = content.match(fwPhasePattern); if (fwMatch) { const insertLine = `\n\t\t\t\t${buildFileId} /* ${SENTRY_PRODUCT} in Frameworks */,`; content = content.replace(fwPhasePattern, `$1${fwMatch[0].includes(buildFileId) ? "" : insertLine}$2`); } else { console.error("Cannot insert into watch Frameworks build phase"); process.exit(1); } // ── 5. packageReferences in PBXProject ── const pkgRefComment = `${pkgRefId} /* XCRemoteSwiftPackageReference "sentry-cocoa" */`; if (!content.includes("packageReferences = (")) { content = content.replace( /(targets\s*=\s*\([^)]*\);)/, `$1\n\t\t\tpackageReferences = (\n\t\t\t\t${pkgRefComment},\n\t\t\t);` ); } else { content = content.replace( /(packageReferences\s*=\s*\()/, `$1\n\t\t\t\t${pkgRefComment},` ); } // ── 6. packageProductDependencies on watch target ── const prodDepComment = `${prodDepId} /* ${SENTRY_PRODUCT} */`; // Find the watch target block and check if packageProductDependencies exists const wtPattern = new RegExp( `(${watchTargetUuid}\\s*\\/\\* SwimBuddy Watch \\*\\/ = \\{[\\s\\S]*?fileSystemSynchronizedGroups = \\([\\s\\S]*?\\);[\\s\\S]*?)(name = )` ); const wtMatch = content.match(wtPattern); if (wtMatch && !content.includes("packageProductDependencies")) { content = content.replace( wtPattern, `$1\n\t\t\tpackageProductDependencies = (\n\t\t\t\t${prodDepComment},\n\t\t\t);\n\t\t$2` ); } fs.writeFileSync(pbxprojPath, content); console.log("SentrySPM added to watch target successfully.");