From 284fd22c136e10aff2c12cbe94f6006a9bbe4122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB?= Date: Sat, 8 Aug 2026 23:21:55 +0200 Subject: [PATCH] chore(watch): automate SentrySPM integration via post-prebuild script --- AGENTS.md | 6 +- package.json | 117 +++++++++++---------- scripts/add-sentry-watch.js | 157 ++++++++++++++++++++++++++++ targets/watch/expo-target.config.js | 8 -- 4 files changed, 220 insertions(+), 68 deletions(-) create mode 100644 scripts/add-sentry-watch.js diff --git a/AGENTS.md b/AGENTS.md index 8f983e9..1f0914d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,13 +8,14 @@ Swimming workout tracker with Apple Watch and WearOS companion apps. Expo 57 + R npx expo start # Dev server npx expo run:ios # Build & run on iOS npx expo prebuild -p ios # Regenerate ios/ dir (safe — targets/ persists) +npx expo prebuild -p ios && node scripts/add-sentry-watch.js # Prebuild + add SentrySPM to watch target npx drizzle-kit generate # Regenerate SQL migrations after schema changes ``` **WearOS:** ```bash -./scripts/build-wearos.sh debug # Build debug APK -./scripts/build-wearos.sh release # Build release APK +./scripts/build-wearos.sh debug # Build debug APK (reads version from app.json) +./scripts/build-wearos.sh release # Build release APK (reads version from app.json) ./scripts/generate-wearos-icon.sh assets/icon.png # Generate launcher icons ``` @@ -58,6 +59,7 @@ targets/wearos/ # WearOS app (Kotlin/Compose) — survives prebuil - WCSession for real-time messaging, `transferUserInfo` for reliable offline sync - HealthKit for swim workout tracking (indoor swimming) - Swift types `StoredWorkout`, `StoredSet`, `CompletedLap` in `WorkoutManager.swift` +- SentrySPM: `@bacons/apple-targets` doesn't support `swiftPackages` yet. Run `node scripts/add-sentry-watch.js` after `expo prebuild` to add it automatically. For EAS builds, this runs automatically via the `eas-build-post-install` hook in package.json. ## WearOS Target diff --git a/package.json b/package.json index 30e7db0..fcdbb77 100644 --- a/package.json +++ b/package.json @@ -1,60 +1,61 @@ { - "name": "swimbuddy", - "version": "1.0.0", - "main": "index.ts", - "dependencies": { - "@bacons/apple-targets": "^5.0.0", - "@expo/vector-icons": "^15.0.2", - "@react-navigation/bottom-tabs": "^7.18.14", - "@react-navigation/native": "^7.3.14", - "@react-navigation/native-stack": "^7.18.6", - "@sentry/react-native": "^8.22.0", - "@tailwindcss/postcss": "^4.3.3", - "drizzle-orm": "^0.45.2", - "expo": "~57.0.10", - "expo-application": "~57.0.2", - "expo-localization": "~57.0.1", - "expo-sqlite": "~57.0.1", - "expo-status-bar": "~57.0.1", - "expo-system-ui": "~57.0.2", - "expo-updates": "^57.0.12", - "i18next": "^26.3.6", - "nativewind": "^5.0.0-preview.4", - "react": "19.2.3", - "react-i18next": "^17.0.11", - "react-native": "0.86.2", - "react-native-css": "^3.0.7", - "react-native-gesture-handler": "~2.32.0", - "react-native-mmkv": "^4.3.2", - "react-native-reanimated": "4.5.1", - "react-native-safe-area-context": "~5.7.0", - "react-native-screens": "~4.26.0" - }, - "devDependencies": { - "@types/react": "~19.2.2", - "babel-plugin-inline-import": "^3.0.0", - "drizzle-kit": "^0.31.10", - "tailwindcss": "^4", - "typescript": "~6.0.3" - }, - "scripts": { - "start": "expo start", - "android": "expo run:android", - "ios": "expo run:ios", - "web": "expo start --web", - "db:generate": "drizzle-kit generate", - "prebuild": "expo prebuild", - "prebuild:clean": "expo prebuild --clean", - "build:wearos:debug": "./scripts/build-wearos.sh debug", - "build:wearos:release": "./scripts/build-wearos.sh release", - "deploy:ios": "eas build --platform ios --profile ${1:-preview} && eas submit --platform ios --profile ${1:-preview}", - "deploy:android": "eas build --platform android --profile ${1:-preview} && eas submit --platform android --profile ${1:-preview}", - "deploy:wearos": "./scripts/build-wearos.sh release ${2:-}", - "update": "eas update --branch ${1:-preview}", - "update:production": "eas update --branch production" - }, - "private": true, - "overrides": { - "lightningcss": "1.30.1" - } + "name": "swimbuddy", + "version": "1.0.0", + "main": "index.ts", + "dependencies": { + "@bacons/apple-targets": "^5.0.0", + "@expo/vector-icons": "^15.0.2", + "@react-navigation/bottom-tabs": "^7.18.14", + "@react-navigation/native": "^7.3.14", + "@react-navigation/native-stack": "^7.18.6", + "@sentry/react-native": "^8.22.0", + "@tailwindcss/postcss": "^4.3.3", + "drizzle-orm": "^0.45.2", + "expo": "~57.0.10", + "expo-application": "~57.0.2", + "expo-localization": "~57.0.1", + "expo-sqlite": "~57.0.1", + "expo-status-bar": "~57.0.1", + "expo-system-ui": "~57.0.2", + "expo-updates": "^57.0.12", + "i18next": "^26.3.6", + "nativewind": "^5.0.0-preview.4", + "react": "19.2.3", + "react-i18next": "^17.0.11", + "react-native": "0.86.2", + "react-native-css": "^3.0.7", + "react-native-gesture-handler": "~2.32.0", + "react-native-mmkv": "^4.3.2", + "react-native-reanimated": "4.5.1", + "react-native-safe-area-context": "~5.7.0", + "react-native-screens": "~4.26.0" + }, + "devDependencies": { + "@types/react": "~19.2.2", + "babel-plugin-inline-import": "^3.0.0", + "drizzle-kit": "^0.31.10", + "tailwindcss": "^4", + "typescript": "~6.0.3" + }, + "scripts": { + "start": "expo start", + "android": "expo run:android", + "ios": "expo run:ios", + "web": "expo start --web", + "db:generate": "drizzle-kit generate", + "prebuild": "expo prebuild && npm run eas-build-post-install", + "prebuild:clean": "expo prebuild --clean && npm run eas-build-post-install", + "eas-build-post-install": "node scripts/add-sentry-watch.js", + "build:wearos:debug": "./scripts/build-wearos.sh debug", + "build:wearos:release": "./scripts/build-wearos.sh release", + "deploy:ios": "eas build --platform ios --profile ${1:-preview} && eas submit --platform ios --profile ${1:-preview}", + "deploy:android": "eas build --platform android --profile ${1:-preview} && eas submit --platform android --profile ${1:-preview}", + "deploy:wearos": "./scripts/build-wearos.sh release ${2:-}", + "update": "eas update --branch ${1:-preview}", + "update:production": "eas update --branch production" + }, + "private": true, + "overrides": { + "lightningcss": "1.30.1" + } } diff --git a/scripts/add-sentry-watch.js b/scripts/add-sentry-watch.js new file mode 100644 index 0000000..17632b0 --- /dev/null +++ b/scripts/add-sentry-watch.js @@ -0,0 +1,157 @@ +#!/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 \*\/\s*=\s*\{\s*\n\s*isa = PBXNativeTarget;\s*\n\s*buildConfigurationList = XX792EB48D81CD7C6C71DCXX/ +); +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 \\*\\/\\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 \*\//, + `/* Begin XCRemoteSwiftPackageReference section */\n${pkgRefLine}\n/* End XCRemoteSwiftPackageReference section */\n/* End PBXProject 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 \*\//, + `/* Begin XCSwiftPackageProductDependency section */\n${prodDepLine}\n/* End XCSwiftPackageProductDependency section */\n/* End XCRemoteSwiftPackageReference 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 \\*\\/ = \\{[\\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."); diff --git a/targets/watch/expo-target.config.js b/targets/watch/expo-target.config.js index 070af9b..b16c9e0 100644 --- a/targets/watch/expo-target.config.js +++ b/targets/watch/expo-target.config.js @@ -5,14 +5,6 @@ module.exports = (config) => ({ icon: "../../assets/icon.png", deploymentTarget: "10.0", frameworks: ["SwiftUI"], - swiftPackages: [ - { - name: "SentrySPM", - repositoryURL: "https://github.com/getsentry/sentry-cocoa.git", - kind: "upToNextMajorVersion", - minimumVersion: "9.25.0", - }, - ], entitlements: { "com.apple.security.application-groups": config.ios?.entitlements?.[