From e68a2872acf5d041083106d149352a60426e5896 Mon Sep 17 00:00:00 2001 From: mario-launchdarkly Date: Fri, 6 Mar 2026 16:19:38 -0600 Subject: [PATCH 1/5] feat: dynamic xcframeworks shipped in package --- .../react-native-ld-session-replay/.gitignore | 7 + .../SessionReplayReactNative.podspec | 35 +- .../app.plugin.js | 1 + .../example/ios/Podfile.lock | 2 +- .../package.json | 18 +- .../plugin/src/index.ts | 35 ++ .../plugin/tsconfig.json | 13 + .../react-native.config.js | 1 + .../scripts/build-xcframeworks.sh | 329 ++++++++++++++++++ 9 files changed, 426 insertions(+), 15 deletions(-) create mode 100644 sdk/@launchdarkly/react-native-ld-session-replay/app.plugin.js create mode 100644 sdk/@launchdarkly/react-native-ld-session-replay/plugin/src/index.ts create mode 100644 sdk/@launchdarkly/react-native-ld-session-replay/plugin/tsconfig.json create mode 100644 sdk/@launchdarkly/react-native-ld-session-replay/react-native.config.js create mode 100755 sdk/@launchdarkly/react-native-ld-session-replay/scripts/build-xcframeworks.sh diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/.gitignore b/sdk/@launchdarkly/react-native-ld-session-replay/.gitignore index d529fdbd0d..0b8bf04f77 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/.gitignore +++ b/sdk/@launchdarkly/react-native-ld-session-replay/.gitignore @@ -78,6 +78,13 @@ android/keystores/debug.keystore # generated by bob lib/ +# XCFrameworks — built artifacts, not committed to git but included in npm publish +# Run: yarn build:xcframeworks +ios/Frameworks/ + +# Expo Config Plugin compiled output +plugin/build/ + # React Native Codegen ios/generated android/generated diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/SessionReplayReactNative.podspec b/sdk/@launchdarkly/react-native-ld-session-replay/SessionReplayReactNative.podspec index 7df4fb0e25..a0a8ac826b 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/SessionReplayReactNative.podspec +++ b/sdk/@launchdarkly/react-native-ld-session-replay/SessionReplayReactNative.podspec @@ -11,22 +11,33 @@ Pod::Spec.new do |s| s.authors = package["author"] s.platforms = { :ios => min_ios_version_supported } - s.source = { :git => "https://github.com/launchdarkly/swift-launchdarkly-observability.git", :tag => "#{s.version}" } + s.source = { :git => "https://github.com/launchdarkly/observability-sdk.git", + :tag => "rn-session-replay-#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,swift,cpp}" s.private_header_files = "ios/**/*.h" - # --- Swift Package Manager Dependency --- - # This helper adds the SPM package to the generated Pods project - spm_dependency( - s, - url: 'https://github.com/launchdarkly/swift-launchdarkly-observability', - requirement: { - kind: 'upToNextMajorVersion', - minimumVersion: '0.18.1' - }, - products: ['LaunchDarklyObservability', 'LaunchDarklySessionReplay'] - ) + # Pre-built XCFrameworks from swift-launchdarkly-observability. + # Built via scripts/build-xcframeworks.sh and shipped inside the npm package. + # This replaces the previous spm_dependency approach, eliminating the need for + # react_native_post_install SPM support and network access at pod install time. + # + # LaunchDarklyObservability is a "fat" static archive that already contains all + # internal and ObjC-only transitive deps (KSCrash, Common, NetworkStatus, etc.). + # The remaining xcframeworks satisfy the Swift module references (@_exported import + # LaunchDarkly / OpenTelemetryApi, plus OtlpConfiguration, ReadableLogRecord, + # SwiftProtobuf.Message) that appear in the public .swiftinterface files. + s.vendored_frameworks = [ + 'ios/Frameworks/LaunchDarklyObservability.xcframework', + 'ios/Frameworks/LaunchDarklySessionReplay.xcframework', + 'ios/Frameworks/LaunchDarkly.xcframework', + 'ios/Frameworks/OpenTelemetryApi.xcframework', + 'ios/Frameworks/OpenTelemetrySdk.xcframework', + 'ios/Frameworks/OpenTelemetryProtocolExporterCommon.xcframework', + 'ios/Frameworks/SwiftProtobuf.xcframework', + ] + + s.pod_target_xcconfig = { 'BUILD_LIBRARY_FOR_DISTRIBUTION' => 'YES' } install_modules_dependencies(s) end diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/app.plugin.js b/sdk/@launchdarkly/react-native-ld-session-replay/app.plugin.js new file mode 100644 index 0000000000..7519dc54fa --- /dev/null +++ b/sdk/@launchdarkly/react-native-ld-session-replay/app.plugin.js @@ -0,0 +1 @@ +module.exports = require('./plugin/build/index.js'); diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/example/ios/Podfile.lock b/sdk/@launchdarkly/react-native-ld-session-replay/example/ios/Podfile.lock index b320ba4a86..88943a6700 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/example/ios/Podfile.lock +++ b/sdk/@launchdarkly/react-native-ld-session-replay/example/ios/Podfile.lock @@ -2802,7 +2802,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: ebcf3a78dc1bcdf054c9e8d309244bade6b31568 ReactCodegen: 11c08ff43a62009d48c71de000352e4515918801 ReactCommon: 424cc34cf5055d69a3dcf02f3436481afb8b0f6f - SessionReplayReactNative: 2abade3c8879121f8316d64bc41d6d29ac3d7d6d + SessionReplayReactNative: 6a469f14f953d03ce8973347cb485524f52517b3 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 6ca93c8c13f56baeec55eb608577619b17a4d64e diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/package.json b/sdk/@launchdarkly/react-native-ld-session-replay/package.json index fb30e20c79..355bd8c6b5 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/package.json +++ b/sdk/@launchdarkly/react-native-ld-session-replay/package.json @@ -1,6 +1,7 @@ { "name": "@launchdarkly/session-replay-react-native", "version": "0.2.1", + "spmVersion": "0.18.1", "description": "session replay for react native", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", @@ -17,7 +18,11 @@ "lib", "android", "ios", + "ios/Frameworks", "cpp", + "plugin", + "app.plugin.js", + "scripts/build-xcframeworks.sh", "*.podspec", "react-native.config.js", "!ios/build", @@ -34,8 +39,10 @@ "scripts": { "example": "yarn workspace session-replay-react-native-example", "build": "bob build", - "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", - "prepack": "bob build", + "build:plugin": "tsc -p plugin/tsconfig.json", + "build:xcframeworks": "bash scripts/build-xcframeworks.sh", + "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib plugin/build", + "prepack": "bob build && tsc -p plugin/tsconfig.json && bash scripts/build-xcframeworks.sh", "typecheck": "tsc", "lint": "echo 'Linting temporarily disabled - TODO: fix ESLint config'", "test": "jest", @@ -72,6 +79,7 @@ "@eslint/compat": "^1.3.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.35.0", + "@expo/config-plugins": "^9.0.0", "@react-native/babel-preset": "0.83.0", "@react-native/eslint-config": "0.83.0", "@react-native/eslint-plugin": "0.83.0", @@ -93,9 +101,15 @@ "typescript": "^5.9.2" }, "peerDependencies": { + "@expo/config-plugins": ">=7.0.0", "react": "*", "react-native": "*" }, + "peerDependenciesMeta": { + "@expo/config-plugins": { + "optional": true + } + }, "workspaces": [ "example" ], diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/plugin/src/index.ts b/sdk/@launchdarkly/react-native-ld-session-replay/plugin/src/index.ts new file mode 100644 index 0000000000..ef822d6ed9 --- /dev/null +++ b/sdk/@launchdarkly/react-native-ld-session-replay/plugin/src/index.ts @@ -0,0 +1,35 @@ +import { ConfigPlugin, withXcodeProject } from '@expo/config-plugins'; + +/** + * Expo Config Plugin for @launchdarkly/session-replay-react-native. + * + * Patches the Xcode project so that dynamic Swift frameworks vendored by the + * pod (LaunchDarklyObservability.xcframework, LaunchDarklySessionReplay.xcframework) + * are embedded correctly in the app bundle. + * + * Usage in app.json / app.config.js: + * "plugins": ["@launchdarkly/session-replay-react-native"] + */ +const withSessionReplay: ConfigPlugin = (config) => { + return withXcodeProject(config, (projectConfig) => { + const project = projectConfig.modResults; + + const buildConfigs = project.pbxBuildConfigurationSection(); + for (const [, buildConfig] of Object.entries(buildConfigs)) { + if ( + typeof buildConfig !== 'object' || + !buildConfig.buildSettings + ) { + continue; + } + // Required when embedding dynamic Swift frameworks into an app that + // may not otherwise include the Swift standard libraries. + buildConfig.buildSettings['ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES'] = + 'YES'; + } + + return projectConfig; + }); +}; + +export default withSessionReplay; diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/plugin/tsconfig.json b/sdk/@launchdarkly/react-native-ld-session-replay/plugin/tsconfig.json new file mode 100644 index 0000000000..44bcf5eda1 --- /dev/null +++ b/sdk/@launchdarkly/react-native-ld-session-replay/plugin/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ES2019", + "module": "commonjs", + "lib": ["ES2019"], + "outDir": "build", + "rootDir": "src", + "declaration": true, + "declarationMap": true + }, + "include": ["src"] +} diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/react-native.config.js b/sdk/@launchdarkly/react-native-ld-session-replay/react-native.config.js new file mode 100644 index 0000000000..f053ebf797 --- /dev/null +++ b/sdk/@launchdarkly/react-native-ld-session-replay/react-native.config.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/scripts/build-xcframeworks.sh b/sdk/@launchdarkly/react-native-ld-session-replay/scripts/build-xcframeworks.sh new file mode 100755 index 0000000000..adcd963f5e --- /dev/null +++ b/sdk/@launchdarkly/react-native-ld-session-replay/scripts/build-xcframeworks.sh @@ -0,0 +1,329 @@ +#!/usr/bin/env bash +# Build LaunchDarklyObservability.xcframework, LaunchDarklySessionReplay.xcframework, +# and all required transitive-dependency xcframeworks from swift-launchdarkly-observability, +# then place them in ios/Frameworks/. +# +# Usage: +# bash scripts/build-xcframeworks.sh [] +# +# Strategy: +# +# BUILD_LIBRARY_FOR_DISTRIBUTION=YES +# Emits .swiftinterface files (text-based, forward-compatible). +# +# OTHER_SWIFT_FLAGS=-no-verify-emitted-module-interface +# Skips post-emit verification of the generated .swiftinterface. +# Avoids a Swift 6.2 / Xcode 26 bug where the NetworkStatus module name +# conflicts with class NetworkStatus.NetworkStatus inside it. +# +# We use 'xcodebuild build' (not 'archive') because SPM schemes under +# 'archive' install products to Products/Users//Objects/ instead of +# the expected Products/Library/Frameworks/ location. +# +# After building LaunchDarklyObservability + LaunchDarklySessionReplay: +# 1. All transitive-dep .o files are merged (libtool) into the +# LaunchDarklyObservability binary so consumers don't need to provide them. +# 2. xcframeworks for the public Swift deps (LaunchDarkly, OpenTelemetry*, +# SwiftProtobuf) are assembled from the same DerivedData so consumers' +# Xcode projects can resolve the module names referenced in our +# .swiftinterface files. +# 3. .swiftinterface files for our two main frameworks are post-processed to +# remove imports that are implementation-only (KSCrash*, Common, +# URLSessionInstrumentation, LDSwiftEventSource) and whose types don't +# appear in the public API — eliminating the need to ship those modules +# as separate xcframeworks. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="${SCRIPT_DIR}/.." +DEST="${ROOT}/ios/Frameworks" +SPM_URL="https://github.com/launchdarkly/swift-launchdarkly-observability" + +# Resolve version: CLI arg → package.json spmVersion → hardcoded fallback +if [[ $# -ge 1 ]]; then + SPM_VERSION="$1" +else + SPM_VERSION="$(node -p "require('${ROOT}/package.json').spmVersion || '0.18.1'" 2>/dev/null || echo '0.18.1')" +fi + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +echo "Building XCFrameworks from swift-launchdarkly-observability @ ${SPM_VERSION}" +echo "Output → ${DEST}" + +# ── Clone ────────────────────────────────────────────────────────────────────── +git clone --depth 1 --branch "${SPM_VERSION}" "${SPM_URL}" "${TMP}/pkg" +cd "${TMP}/pkg" +mkdir -p "${DEST}" + +# Shared DerivedData directories (reused across schemes so the second scheme +# benefits from already-compiled dependencies). +IOS_DD="${TMP}/DD-iOS" +SIM_DD="${TMP}/DD-Sim" + +# ── Helper: locate or create a static archive from build products ────────────── +# resolve_archive +resolve_archive() { + local SCHEME="$1" PRODS="$2" OUT_A="$3" + if [[ -f "${PRODS}/${SCHEME}.a" ]]; then cp "${PRODS}/${SCHEME}.a" "${OUT_A}" + elif [[ -f "${PRODS}/lib${SCHEME}.a" ]]; then cp "${PRODS}/lib${SCHEME}.a" "${OUT_A}" + else + local OBJ="${PRODS}/${SCHEME}.o" + if [[ ! -f "${OBJ}" ]]; then + echo " ✗ No .a or .o found for ${SCHEME} under ${PRODS}" + find "${PRODS}" -maxdepth 4 2>/dev/null | head -40 || true + exit 1 + fi + libtool -static -o "${OUT_A}" "${OBJ}" + fi +} + +# ── Helper: assemble a minimal .framework bundle ─────────────────────────────── +# make_fw_slice +make_fw_slice() { + local SCHEME="$1" FW_DIR="$2" ARCH_A="$3" PRODS_DIR="$4" + mkdir -p "${FW_DIR}/Modules" "${FW_DIR}/Headers" + + cp "${ARCH_A}" "${FW_DIR}/${SCHEME}" + + # Copy the full .swiftmodule directory. + local SWIFTMOD="${PRODS_DIR}/${SCHEME}.swiftmodule" + if [[ -d "${SWIFTMOD}" ]]; then + cp -r "${SWIFTMOD}" "${FW_DIR}/Modules/${SCHEME}.swiftmodule" + fi + + # Minimal module map (pure-Swift framework; no umbrella header needed) + printf 'framework module %s {\n export *\n}\n' "${SCHEME}" \ + > "${FW_DIR}/Modules/module.modulemap" + + /usr/libexec/PlistBuddy \ + -c "Add :CFBundleExecutable string ${SCHEME}" \ + -c "Add :CFBundleIdentifier string com.launchdarkly.${SCHEME}" \ + -c "Add :CFBundleName string ${SCHEME}" \ + -c "Add :CFBundlePackageType string FMWK" \ + -c "Add :CFBundleVersion string 1" \ + -c "Add :MinimumOSVersion string 16.0" \ + "${FW_DIR}/Info.plist" +} + +# ── Helper: strip implementation-only imports from all .swiftinterface files ── +# strip_imports [ ...] +strip_imports() { + local XCF="$1"; shift + local IMPORTS=("$@") + + # Build a sed expression that removes each listed import line + local SED_EXPR="" + for IMP in "${IMPORTS[@]}"; do + SED_EXPR="${SED_EXPR}/^import ${IMP}$/d;" + done + + find "${XCF}" -name "*.swiftinterface" | while read -r F; do + sed -i '' "${SED_EXPR}" "${F}" + done +} + +# ── Build one XCFramework (runs xcodebuild for the named scheme) ─────────────── +# build_xcframework [] +# +# Extra .o files (space-separated names without .o) are merged into the binary. +# This is used to fold implementation-only deps whose imports we strip. +build_xcframework() { + local SCHEME="$1"; shift + local EXTRA_DEPS=("$@") + local OUT="${DEST}/${SCHEME}.xcframework" + local IOS_PRODS="${IOS_DD}/Build/Products/Release-iphoneos" + local SIM_PRODS="${SIM_DD}/Build/Products/Release-iphonesimulator" + + echo "" + echo " → Building ${SCHEME} for iOS device..." + xcodebuild build \ + -scheme "${SCHEME}" \ + -destination "generic/platform=iOS" \ + -derivedDataPath "${IOS_DD}" \ + -configuration Release \ + BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ + ENABLE_BITCODE=NO \ + OTHER_SWIFT_FLAGS="-no-verify-emitted-module-interface" \ + SWIFT_TREAT_WARNINGS_AS_ERRORS=NO \ + -quiet + + echo " → Building ${SCHEME} for iOS Simulator..." + xcodebuild build \ + -scheme "${SCHEME}" \ + -destination "generic/platform=iOS Simulator" \ + -derivedDataPath "${SIM_DD}" \ + -configuration Release \ + BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ + ENABLE_BITCODE=NO \ + OTHER_SWIFT_FLAGS="-no-verify-emitted-module-interface" \ + SWIFT_TREAT_WARNINGS_AS_ERRORS=NO \ + -quiet + + echo " → Packaging ${SCHEME}.xcframework..." + + local IOS_A="${TMP}/${SCHEME}-ios.a" + local SIM_A="${TMP}/${SCHEME}-sim.a" + resolve_archive "${SCHEME}" "${IOS_PRODS}" "${IOS_A}" + resolve_archive "${SCHEME}" "${SIM_PRODS}" "${SIM_A}" + + # Merge extra dep .o files into the archive so consumers don't need them + if [[ ${#EXTRA_DEPS[@]} -gt 0 ]]; then + local IOS_EXTRA=() + local SIM_EXTRA=() + for DEP in "${EXTRA_DEPS[@]}"; do + [[ -f "${IOS_PRODS}/${DEP}.o" ]] && IOS_EXTRA+=("${IOS_PRODS}/${DEP}.o") + [[ -f "${IOS_PRODS}/${DEP}.a" ]] && IOS_EXTRA+=("${IOS_PRODS}/${DEP}.a") + [[ -f "${SIM_PRODS}/${DEP}.o" ]] && SIM_EXTRA+=("${SIM_PRODS}/${DEP}.o") + [[ -f "${SIM_PRODS}/${DEP}.a" ]] && SIM_EXTRA+=("${SIM_PRODS}/${DEP}.a") + done + if [[ ${#IOS_EXTRA[@]} -gt 0 ]]; then + libtool -static -o "${IOS_A}" "${IOS_A}" "${IOS_EXTRA[@]}" + fi + if [[ ${#SIM_EXTRA[@]} -gt 0 ]]; then + libtool -static -o "${SIM_A}" "${SIM_A}" "${SIM_EXTRA[@]}" + fi + fi + + local IOS_FW="${TMP}/${SCHEME}-iOS/${SCHEME}.framework" + local SIM_FW="${TMP}/${SCHEME}-Sim/${SCHEME}.framework" + make_fw_slice "${SCHEME}" "${IOS_FW}" "${IOS_A}" "${IOS_PRODS}" + make_fw_slice "${SCHEME}" "${SIM_FW}" "${SIM_A}" "${SIM_PRODS}" + + rm -rf "${OUT}" + xcodebuild -create-xcframework \ + -framework "${IOS_FW}" \ + -framework "${SIM_FW}" \ + -output "${OUT}" + + echo " ✓ ${OUT}" +} + +# ── Package a dep XCFramework from already-built DerivedData (no xcodebuild) ── +# package_dep_xcframework [] +# +# Assembles an xcframework for a module that was compiled as a transitive dep +# when building LaunchDarklyObservability. DerivedData (IOS_DD / SIM_DD) must +# already be populated. +package_dep_xcframework() { + local MODULE="$1"; shift + local EXTRA_DEPS=("$@") + local OUT="${DEST}/${MODULE}.xcframework" + local IOS_PRODS="${IOS_DD}/Build/Products/Release-iphoneos" + local SIM_PRODS="${SIM_DD}/Build/Products/Release-iphonesimulator" + + echo " → Packaging dep ${MODULE}.xcframework..." + + local IOS_A="${TMP}/${MODULE}-ios.a" + local SIM_A="${TMP}/${MODULE}-sim.a" + resolve_archive "${MODULE}" "${IOS_PRODS}" "${IOS_A}" + resolve_archive "${MODULE}" "${SIM_PRODS}" "${SIM_A}" + + # Merge extra deps + if [[ ${#EXTRA_DEPS[@]} -gt 0 ]]; then + local IOS_EXTRA=() + local SIM_EXTRA=() + for DEP in "${EXTRA_DEPS[@]}"; do + [[ -f "${IOS_PRODS}/${DEP}.o" ]] && IOS_EXTRA+=("${IOS_PRODS}/${DEP}.o") + [[ -f "${IOS_PRODS}/${DEP}.a" ]] && IOS_EXTRA+=("${IOS_PRODS}/${DEP}.a") + [[ -f "${SIM_PRODS}/${DEP}.o" ]] && SIM_EXTRA+=("${SIM_PRODS}/${DEP}.o") + [[ -f "${SIM_PRODS}/${DEP}.a" ]] && SIM_EXTRA+=("${SIM_PRODS}/${DEP}.a") + done + if [[ ${#IOS_EXTRA[@]} -gt 0 ]]; then + libtool -static -o "${IOS_A}" "${IOS_A}" "${IOS_EXTRA[@]}" + fi + if [[ ${#SIM_EXTRA[@]} -gt 0 ]]; then + libtool -static -o "${SIM_A}" "${SIM_A}" "${SIM_EXTRA[@]}" + fi + fi + + local IOS_FW="${TMP}/${MODULE}-iOS/${MODULE}.framework" + local SIM_FW="${TMP}/${MODULE}-Sim/${MODULE}.framework" + make_fw_slice "${MODULE}" "${IOS_FW}" "${IOS_A}" "${IOS_PRODS}" + make_fw_slice "${MODULE}" "${SIM_FW}" "${SIM_A}" "${SIM_PRODS}" + + rm -rf "${OUT}" + xcodebuild -create-xcframework \ + -framework "${IOS_FW}" \ + -framework "${SIM_FW}" \ + -output "${OUT}" + + echo " ✓ ${OUT}" +} + +# ── Step 1: Build the two main frameworks ───────────────────────────────────── +# +# LaunchDarklyObservability is built first (SessionReplay depends on it). +# We fold in all internal / ObjC-only deps whose imports we will strip: +# Common, NetworkStatus, ObjCBridge, URLSessionInstrumentation, +# KSCrash* (KSCrashCore, KSCrashDemangleFilter, KSCrashFilters, +# KSCrashInstallations, KSCrashRecording, KSCrashRecordingCore, +# KSCrashReportingCore, KSCrashSinks) +build_xcframework "LaunchDarklyObservability" \ + Common \ + NetworkStatus \ + ObjCBridge \ + URLSessionInstrumentation \ + KSCrashCore \ + KSCrashDemangleFilter \ + KSCrashFilters \ + KSCrashInstallations \ + KSCrashRecording \ + KSCrashRecordingCore \ + KSCrashReportingCore \ + KSCrashSinks + +build_xcframework "LaunchDarklySessionReplay" + +# ── Step 2: Post-process swiftinterfaces — strip implementation-only imports ── +# +# These imports appear in the generated .swiftinterface even though the types +# from those modules are NOT part of the public API (they're used only in +# implementation files). Stripping them means consumers no longer need to +# resolve those modules at compile time. The compiled code is already folded +# into LaunchDarklyObservability.xcframework (step 1) so link-time is fine. + +echo "" +echo " → Stripping implementation-only imports from swiftinterfaces..." + +# LaunchDarklyObservability: strip KSCrash*, Common, URLSessionInstrumentation +strip_imports "${DEST}/LaunchDarklyObservability.xcframework" \ + Common \ + KSCrashDemangleFilter \ + KSCrashFilters \ + KSCrashInstallations \ + KSCrashRecording \ + URLSessionInstrumentation + +# LaunchDarklySessionReplay: strip Common +strip_imports "${DEST}/LaunchDarklySessionReplay.xcframework" \ + Common + +# ── Step 3: Build xcframeworks for public transitive deps ────────────────────── +# +# These modules ARE referenced by types in the public API of our frameworks +# (e.g. @_exported import LaunchDarkly, OpenTelemetryApi; OtlpConfiguration; +# ReadableLogRecord; SwiftProtobuf.Message). We fold LDSwiftEventSource (a +# LaunchDarkly implementation detail) into the LaunchDarkly binary and strip +# its import from LaunchDarkly's .swiftinterface. + +echo "" +echo "Building transitive-dependency xcframeworks..." + +package_dep_xcframework "LaunchDarkly" LDSwiftEventSource +# Strip LDSwiftEventSource from LaunchDarkly's swiftinterface — +# it's not part of the public LaunchDarkly API. +strip_imports "${DEST}/LaunchDarkly.xcframework" LDSwiftEventSource + +package_dep_xcframework "OpenTelemetryApi" +package_dep_xcframework "OpenTelemetrySdk" +package_dep_xcframework "OpenTelemetryProtocolExporterCommon" +package_dep_xcframework "SwiftProtobuf" + +echo "" +echo "✓ XCFrameworks ready in ${DEST}" +echo "" +echo "Next steps:" +echo " 1. Run: cd example/ios && pod install" +echo " 2. Build the example app on device and simulator" From a80dd61f7db49c5f91f106a0304ad63121dd24c5 Mon Sep 17 00:00:00 2001 From: mario-launchdarkly Date: Fri, 6 Mar 2026 16:44:21 -0600 Subject: [PATCH 2/5] chore: bump version of session replay package to 0.2.2 --- sdk/@launchdarkly/react-native-ld-session-replay/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/package.json b/sdk/@launchdarkly/react-native-ld-session-replay/package.json index 355bd8c6b5..6ec589477e 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/package.json +++ b/sdk/@launchdarkly/react-native-ld-session-replay/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/session-replay-react-native", - "version": "0.2.1", + "version": "0.2.2", "spmVersion": "0.18.1", "description": "session replay for react native", "main": "./lib/module/index.js", From 01390c67db7764705df62fa59f8d613731a50b1e Mon Sep 17 00:00:00 2001 From: mario-launchdarkly Date: Fri, 6 Mar 2026 16:46:43 -0600 Subject: [PATCH 3/5] chore: update package versions for observability SDKs - Bump @launchdarkly/observability-android to 0.29.0, adding MAUI hook proxy and fixing fullsnapshot nodeId reset. - Bump @launchdarkly/observability-react-native to 0.7.1, addressing ldclient dependency issues. - Bump @launchdarkly/session-replay-react-native to 0.2.2, fixing ldclient dependencies and updating workspace dependency on observability-react-native. --- yarn.lock | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index e450642be3..371921e2b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5191,6 +5191,28 @@ __metadata: languageName: node linkType: hard +"@expo/config-plugins@npm:^9.0.0": + version: 9.1.7 + resolution: "@expo/config-plugins@npm:9.1.7" + dependencies: + "@expo/config-types": "npm:^53.0.0" + "@expo/json-file": "npm:~9.1.3" + "@expo/plist": "npm:^0.3.3" + "@expo/sdk-runtime-versions": "npm:^1.0.0" + chalk: "npm:^4.1.2" + debug: "npm:^4.3.5" + getenv: "npm:^1.0.0" + glob: "npm:^10.4.2" + resolve-from: "npm:^5.0.0" + semver: "npm:^7.5.4" + slash: "npm:^3.0.0" + slugify: "npm:^1.6.6" + xcode: "npm:^3.0.1" + xml2js: "npm:0.6.0" + checksum: 10/2ca6021045b6620eb258f8bc7933e99cff9a4fab60e5ff5ccf95ed757df12395ddf2ccb5fa5ad92c606defd999e59bc675fc2b778e373cc04b5abc139e9ecaeb + languageName: node + linkType: hard + "@expo/config-plugins@npm:~10.0.2, @expo/config-plugins@npm:~10.0.3": version: 10.0.3 resolution: "@expo/config-plugins@npm:10.0.3" @@ -5242,6 +5264,13 @@ __metadata: languageName: node linkType: hard +"@expo/config-types@npm:^53.0.0": + version: 53.0.5 + resolution: "@expo/config-types@npm:53.0.5" + checksum: 10/71971858185b6163459271734903258c9cdd26a0ffc9775d038f37ebb71ab07153494b0157b96eed03600789592862458e81dfbcc8ef440d28fdcf965f0ba012 + languageName: node + linkType: hard + "@expo/config-types@npm:^53.0.4": version: 53.0.4 resolution: "@expo/config-types@npm:53.0.4" @@ -5414,7 +5443,7 @@ __metadata: languageName: node linkType: hard -"@expo/json-file@npm:^9.0.2, @expo/json-file@npm:^9.1.4, @expo/json-file@npm:^9.1.5, @expo/json-file@npm:~9.1.4": +"@expo/json-file@npm:^9.0.2, @expo/json-file@npm:^9.1.4, @expo/json-file@npm:^9.1.5, @expo/json-file@npm:~9.1.3, @expo/json-file@npm:~9.1.4": version: 9.1.5 resolution: "@expo/json-file@npm:9.1.5" dependencies: @@ -5674,6 +5703,17 @@ __metadata: languageName: node linkType: hard +"@expo/plist@npm:^0.3.3": + version: 0.3.5 + resolution: "@expo/plist@npm:0.3.5" + dependencies: + "@xmldom/xmldom": "npm:^0.8.8" + base64-js: "npm:^1.2.3" + xmlbuilder: "npm:^15.1.1" + checksum: 10/a79f11e21c0072baf32444a0ca38883f966470df44d7bd30b244e4dba2aa1c66e186129d22c1ed7fd4139fd053ec9ae3d9d41a4f6fc36f51306a5b9a455d7676 + languageName: node + linkType: hard + "@expo/plist@npm:^0.3.4": version: 0.3.4 resolution: "@expo/plist@npm:0.3.4" @@ -8884,6 +8924,7 @@ __metadata: "@eslint/compat": "npm:^1.3.2" "@eslint/eslintrc": "npm:^3.3.1" "@eslint/js": "npm:^9.35.0" + "@expo/config-plugins": "npm:^9.0.0" "@launchdarkly/js-sdk-common": "npm:^2.20.0" "@launchdarkly/observability-react-native": "workspace:*" "@launchdarkly/react-native-client-sdk": "npm:^10.12.5" @@ -8907,8 +8948,12 @@ __metadata: turbo: "npm:^2.5.6" typescript: "npm:^5.9.2" peerDependencies: + "@expo/config-plugins": ">=7.0.0" react: "*" react-native: "*" + peerDependenciesMeta: + "@expo/config-plugins": + optional: true languageName: unknown linkType: soft From d7443cf40b627788d783c39f5a143112ecac8d27 Mon Sep 17 00:00:00 2001 From: mario-launchdarkly Date: Fri, 6 Mar 2026 17:02:06 -0600 Subject: [PATCH 4/5] fix: clean script omits new ios frameworks build artifact --- sdk/@launchdarkly/react-native-ld-session-replay/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/package.json b/sdk/@launchdarkly/react-native-ld-session-replay/package.json index 6ec589477e..5ef6055058 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/package.json +++ b/sdk/@launchdarkly/react-native-ld-session-replay/package.json @@ -41,7 +41,7 @@ "build": "bob build", "build:plugin": "tsc -p plugin/tsconfig.json", "build:xcframeworks": "bash scripts/build-xcframeworks.sh", - "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib plugin/build", + "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib plugin/build ios/Frameworks", "prepack": "bob build && tsc -p plugin/tsconfig.json && bash scripts/build-xcframeworks.sh", "typecheck": "tsc", "lint": "echo 'Linting temporarily disabled - TODO: fix ESLint config'", From cd7815d7418e8291ee6960ec3d566df1a973fbdb Mon Sep 17 00:00:00 2001 From: mario-launchdarkly Date: Fri, 6 Mar 2026 17:19:39 -0600 Subject: [PATCH 5/5] fix: duplicated framework packaging logic across two functions --- .../scripts/build-xcframeworks.sh | 141 +++++++----------- 1 file changed, 54 insertions(+), 87 deletions(-) diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/scripts/build-xcframeworks.sh b/sdk/@launchdarkly/react-native-ld-session-replay/scripts/build-xcframeworks.sh index adcd963f5e..2db0ba349d 100755 --- a/sdk/@launchdarkly/react-native-ld-session-replay/scripts/build-xcframeworks.sh +++ b/sdk/@launchdarkly/react-native-ld-session-replay/scripts/build-xcframeworks.sh @@ -124,18 +124,60 @@ strip_imports() { done } -# ── Build one XCFramework (runs xcodebuild for the named scheme) ─────────────── -# build_xcframework [] +# ── Helper: package an XCFramework from already-populated DerivedData ───────── +# package_xcframework [] # -# Extra .o files (space-separated names without .o) are merged into the binary. -# This is used to fold implementation-only deps whose imports we strip. -build_xcframework() { - local SCHEME="$1"; shift +# Resolves the module's archive, optionally merges extra dep .o/.a files into +# it, assembles iOS-device and Simulator .framework slices, then calls +# xcodebuild -create-xcframework. DerivedData (IOS_DD / SIM_DD) must already +# contain build products for before this is called. +package_xcframework() { + local MODULE="$1"; shift local EXTRA_DEPS=("$@") - local OUT="${DEST}/${SCHEME}.xcframework" + local OUT="${DEST}/${MODULE}.xcframework" local IOS_PRODS="${IOS_DD}/Build/Products/Release-iphoneos" local SIM_PRODS="${SIM_DD}/Build/Products/Release-iphonesimulator" + local IOS_A="${TMP}/${MODULE}-ios.a" + local SIM_A="${TMP}/${MODULE}-sim.a" + resolve_archive "${MODULE}" "${IOS_PRODS}" "${IOS_A}" + resolve_archive "${MODULE}" "${SIM_PRODS}" "${SIM_A}" + + # Merge extra dep .o/.a files into the archive so consumers don't need them + if [[ ${#EXTRA_DEPS[@]} -gt 0 ]]; then + local IOS_EXTRA=() SIM_EXTRA=() + for DEP in "${EXTRA_DEPS[@]}"; do + [[ -f "${IOS_PRODS}/${DEP}.o" ]] && IOS_EXTRA+=("${IOS_PRODS}/${DEP}.o") + [[ -f "${IOS_PRODS}/${DEP}.a" ]] && IOS_EXTRA+=("${IOS_PRODS}/${DEP}.a") + [[ -f "${SIM_PRODS}/${DEP}.o" ]] && SIM_EXTRA+=("${SIM_PRODS}/${DEP}.o") + [[ -f "${SIM_PRODS}/${DEP}.a" ]] && SIM_EXTRA+=("${SIM_PRODS}/${DEP}.a") + done + [[ ${#IOS_EXTRA[@]} -gt 0 ]] && libtool -static -o "${IOS_A}" "${IOS_A}" "${IOS_EXTRA[@]}" + [[ ${#SIM_EXTRA[@]} -gt 0 ]] && libtool -static -o "${SIM_A}" "${SIM_A}" "${SIM_EXTRA[@]}" + fi + + local IOS_FW="${TMP}/${MODULE}-iOS/${MODULE}.framework" + local SIM_FW="${TMP}/${MODULE}-Sim/${MODULE}.framework" + make_fw_slice "${MODULE}" "${IOS_FW}" "${IOS_A}" "${IOS_PRODS}" + make_fw_slice "${MODULE}" "${SIM_FW}" "${SIM_A}" "${SIM_PRODS}" + + rm -rf "${OUT}" + xcodebuild -create-xcframework \ + -framework "${IOS_FW}" \ + -framework "${SIM_FW}" \ + -output "${OUT}" + + echo " ✓ ${OUT}" +} + +# ── Build one XCFramework (runs xcodebuild for the named scheme) ─────────────── +# build_xcframework [] +# +# Compiles for iOS device and Simulator, then delegates to +# package_xcframework for artifact collection and xcframework assembly. +build_xcframework() { + local SCHEME="$1"; shift + echo "" echo " → Building ${SCHEME} for iOS device..." xcodebuild build \ @@ -162,94 +204,19 @@ build_xcframework() { -quiet echo " → Packaging ${SCHEME}.xcframework..." - - local IOS_A="${TMP}/${SCHEME}-ios.a" - local SIM_A="${TMP}/${SCHEME}-sim.a" - resolve_archive "${SCHEME}" "${IOS_PRODS}" "${IOS_A}" - resolve_archive "${SCHEME}" "${SIM_PRODS}" "${SIM_A}" - - # Merge extra dep .o files into the archive so consumers don't need them - if [[ ${#EXTRA_DEPS[@]} -gt 0 ]]; then - local IOS_EXTRA=() - local SIM_EXTRA=() - for DEP in "${EXTRA_DEPS[@]}"; do - [[ -f "${IOS_PRODS}/${DEP}.o" ]] && IOS_EXTRA+=("${IOS_PRODS}/${DEP}.o") - [[ -f "${IOS_PRODS}/${DEP}.a" ]] && IOS_EXTRA+=("${IOS_PRODS}/${DEP}.a") - [[ -f "${SIM_PRODS}/${DEP}.o" ]] && SIM_EXTRA+=("${SIM_PRODS}/${DEP}.o") - [[ -f "${SIM_PRODS}/${DEP}.a" ]] && SIM_EXTRA+=("${SIM_PRODS}/${DEP}.a") - done - if [[ ${#IOS_EXTRA[@]} -gt 0 ]]; then - libtool -static -o "${IOS_A}" "${IOS_A}" "${IOS_EXTRA[@]}" - fi - if [[ ${#SIM_EXTRA[@]} -gt 0 ]]; then - libtool -static -o "${SIM_A}" "${SIM_A}" "${SIM_EXTRA[@]}" - fi - fi - - local IOS_FW="${TMP}/${SCHEME}-iOS/${SCHEME}.framework" - local SIM_FW="${TMP}/${SCHEME}-Sim/${SCHEME}.framework" - make_fw_slice "${SCHEME}" "${IOS_FW}" "${IOS_A}" "${IOS_PRODS}" - make_fw_slice "${SCHEME}" "${SIM_FW}" "${SIM_A}" "${SIM_PRODS}" - - rm -rf "${OUT}" - xcodebuild -create-xcframework \ - -framework "${IOS_FW}" \ - -framework "${SIM_FW}" \ - -output "${OUT}" - - echo " ✓ ${OUT}" + package_xcframework "${SCHEME}" "$@" } # ── Package a dep XCFramework from already-built DerivedData (no xcodebuild) ── # package_dep_xcframework [] # -# Assembles an xcframework for a module that was compiled as a transitive dep -# when building LaunchDarklyObservability. DerivedData (IOS_DD / SIM_DD) must -# already be populated. +# Assembles an xcframework for a module compiled as a transitive dep of a +# previously-built scheme. DerivedData (IOS_DD / SIM_DD) must already be +# populated. package_dep_xcframework() { local MODULE="$1"; shift - local EXTRA_DEPS=("$@") - local OUT="${DEST}/${MODULE}.xcframework" - local IOS_PRODS="${IOS_DD}/Build/Products/Release-iphoneos" - local SIM_PRODS="${SIM_DD}/Build/Products/Release-iphonesimulator" - echo " → Packaging dep ${MODULE}.xcframework..." - - local IOS_A="${TMP}/${MODULE}-ios.a" - local SIM_A="${TMP}/${MODULE}-sim.a" - resolve_archive "${MODULE}" "${IOS_PRODS}" "${IOS_A}" - resolve_archive "${MODULE}" "${SIM_PRODS}" "${SIM_A}" - - # Merge extra deps - if [[ ${#EXTRA_DEPS[@]} -gt 0 ]]; then - local IOS_EXTRA=() - local SIM_EXTRA=() - for DEP in "${EXTRA_DEPS[@]}"; do - [[ -f "${IOS_PRODS}/${DEP}.o" ]] && IOS_EXTRA+=("${IOS_PRODS}/${DEP}.o") - [[ -f "${IOS_PRODS}/${DEP}.a" ]] && IOS_EXTRA+=("${IOS_PRODS}/${DEP}.a") - [[ -f "${SIM_PRODS}/${DEP}.o" ]] && SIM_EXTRA+=("${SIM_PRODS}/${DEP}.o") - [[ -f "${SIM_PRODS}/${DEP}.a" ]] && SIM_EXTRA+=("${SIM_PRODS}/${DEP}.a") - done - if [[ ${#IOS_EXTRA[@]} -gt 0 ]]; then - libtool -static -o "${IOS_A}" "${IOS_A}" "${IOS_EXTRA[@]}" - fi - if [[ ${#SIM_EXTRA[@]} -gt 0 ]]; then - libtool -static -o "${SIM_A}" "${SIM_A}" "${SIM_EXTRA[@]}" - fi - fi - - local IOS_FW="${TMP}/${MODULE}-iOS/${MODULE}.framework" - local SIM_FW="${TMP}/${MODULE}-Sim/${MODULE}.framework" - make_fw_slice "${MODULE}" "${IOS_FW}" "${IOS_A}" "${IOS_PRODS}" - make_fw_slice "${MODULE}" "${SIM_FW}" "${SIM_A}" "${SIM_PRODS}" - - rm -rf "${OUT}" - xcodebuild -create-xcframework \ - -framework "${IOS_FW}" \ - -framework "${SIM_FW}" \ - -output "${OUT}" - - echo " ✓ ${OUT}" + package_xcframework "${MODULE}" "$@" } # ── Step 1: Build the two main frameworks ─────────────────────────────────────