Skip to content

Commit 3f3a5a3

Browse files
LeslieOAclaude
andauthored
feat(macos): ship macOS support via RN's first-party SPM helper (#30)
The original blocker for #27 wasn't Swift sources — it was cocoapods-spm 0.1.20 producing a malformed Pods.xcodeproj on Xcode 26 (UUID collision between its STTextView XCSwiftPackageProductDependency and the PBXProject root, causing Xcode to send `_setSavedArchiveVersion:` to the wrong type and bail). Issue trinhngocthuyen/cocoapods-spm#172 tracks it; no upstream fix and the RN community is migrating off the plugin. Switch the macOS path to React Native's first-party `spm_dependency` helper (lives in `react_native_pods.rb`, present in react-native-macos 0.81). It registers SPM products via Xcodeproj-direct, sidestepping the cocoapods-spm UUID allocator entirely. iOS-via-Expo-CNG keeps using cocoapods-spm via `app.plugin.js` — that path was never broken. Library - `ReactNativeSourceEditor.podspec`: add `:osx => '14.0'` and gate the SPM declaration on `ENV['RNSE_USE_RN_SPM']`. iOS branch keeps `s.spm_dependency 'STTextView/STTextView'` (cocoapods-spm); macOS branch calls RN's top-level `spm_dependency s, url:, requirement:, products:`. The gate can't be `s.respond_to?(:spm_dependency)` — cocoapods-spm monkey-patches Pod::Specification at gem load time globally whenever installed. - `ios/SourceEditor.mm`: guard the Swift bridging header import with `__has_include`. iOS gets `<ReactNativeSourceEditor/...>` (framework wrapper exists under `use_frameworks!`); macOS falls back to quote-form which picks up the header from DerivedSources/ (no .framework wrapper under default static-library linkage). Example app - `macos/Podfile`: drop `plugin 'cocoapods-spm'` and `spm_pkg`; set `ENV['RNSE_USE_RN_SPM'] = '1'`. Stay on static linkage (the SPM warning recommends dynamic, but `use_frameworks! :linkage => :dynamic` triggers a separate RN-macOS bug where React-Core's `RCTView.m` hardcodes a class ref to `RCTTextView` that doesn't resolve across dynamic frameworks). - `metro.config.js`: add `unstable_enablePackageExports` and `unstable_conditionNames: ['source', 'react-native', ...]` so metro reads the package's `source` exports condition (`src/index.ts`) instead of the unbuilt `lib/module/index.js`. Expo's metro config does this automatically for the iOS example; @react-native/metro-config does not. - `Info.plist`: add `RCTNewArchEnabled = true` for Fabric. - `MacosApp.xcodeproj`: Pods integration auto-edits from `pod install`. - `Podfile.lock`, `PrivacyInfo.xcprivacy`: track for reproducibility. - `README.md`: rewrite — now ships, documents toolchain choices. Hygiene - `.gitignore`: add `Pods/`, `MacosApp.xcworkspace/`, `.xcode.env*`, `.spm.pods/` under `example/macos-app/macos/`. - `package.json`: drop `macos:plugin` (installed cocoapods-spm — no longer needed on the macOS path). Closes #27. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 7eb09fb commit 3f3a5a3

11 files changed

Lines changed: 2860 additions & 42 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ android/keystores/debug.keystore
4242
# Cocoapods
4343
#
4444
example/ios/Pods
45+
example/macos-app/macos/Pods/
46+
example/macos-app/macos/MacosApp.xcworkspace/
47+
example/macos-app/macos/.xcode.env*
48+
example/macos-app/macos/.spm.pods/
4549

4650
# Ruby
4751
example/vendor/

ReactNativeSourceEditor.podspec

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Pod::Spec.new do |s|
1010
s.license = package["license"]
1111
s.authors = package["author"]
1212

13-
s.platforms = { :ios => '16.0' }
13+
s.platforms = { :ios => '16.0', :osx => '14.0' }
1414
s.source = { :git => "https://github.com/workspace-sh/react-native-source-editor.git", :tag => "#{s.version}" }
1515

1616
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
@@ -30,7 +30,32 @@ Pod::Spec.new do |s|
3030
s.static_framework = true
3131
s.header_dir = 'ReactNativeSourceEditor'
3232

33-
s.spm_dependency 'STTextView/STTextView'
33+
# STTextView is SPM-only. Two integration paths, picked by ENV from the
34+
# consumer Podfile (NOT by `s.respond_to?` — the cocoapods-spm gem
35+
# monkey-patches Pod::Specification at gem load time whenever it's
36+
# installed, so `respond_to?` can't tell us whether it's actually wired
37+
# into this Podfile):
38+
#
39+
# 1. iOS via Expo CNG (default) — `app.plugin.js` injects the
40+
# `cocoapods-spm` plugin into the iOS Podfile, which registers
41+
# STTextView via `spm_pkg`. The library calls the plugin's
42+
# `s.spm_dependency` to bind the package to this pod.
43+
#
44+
# 2. Bare RN (react-native-macos) — consumer Podfile sets
45+
# `ENV['RNSE_USE_RN_SPM'] = '1'` and uses RN's first-party
46+
# `spm_dependency` top-level helper from react_native_pods.rb. This
47+
# bypasses cocoapods-spm entirely (it has an unfixed Xcode 26
48+
# regression — issue #172). Consumers must also set
49+
# `use_frameworks! :linkage => :dynamic` so the SwiftPackage product
50+
# links cleanly into the Pods project.
51+
if ENV['RNSE_USE_RN_SPM'] == '1'
52+
spm_dependency s,
53+
url: 'https://github.com/krzyzanowskim/STTextView',
54+
requirement: { kind: 'upToNextMajorVersion', minimumVersion: '2.3.10' },
55+
products: ['STTextView']
56+
else
57+
s.spm_dependency 'STTextView/STTextView'
58+
end
3459

3560
s.pod_target_xcconfig = {
3661
'DEFINES_MODULE' => 'YES',

example/macos-app/README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
# macOS example app
22

3-
> **Status:** scaffolded but not yet runnable — pending [#27](https://github.com/workspace-sh/react-native-source-editor/issues/27).
3+
A [react-native-macos](https://github.com/microsoft/react-native-macos) 0.81 host for verifying `@workspace-sh/react-native-source-editor` on macOS, alongside the iOS Expo CNG example in `example/ios-app/`.
44

5-
This directory holds a [react-native-macos](https://github.com/microsoft/react-native-macos) 0.81 host that we plan to use for verifying `@workspace-sh/react-native-source-editor` on macOS, alongside the iOS Expo CNG example in `example/ios-app/`.
5+
## Run it
66

7-
It is **not currently buildable** because the library still ships Swift sources (`ios/Highlighter.swift`, `ios/SourceEditorImpl.swift`). On RN-macos 0.81, a Swift-bearing CocoaPods pod fails with `module map file 'ReactNativeSourceEditor.modulemap' not found` at consumer link time — the modulemap CocoaPods generates for the Swift module never lands in `${PODS_CONFIGURATION_BUILD_DIR}/<framework>/` where the consumer's import path resolves. iOS works fine because Expo CNG's prebuild orchestrates the pod install differently.
7+
From the repo root:
88

9-
The plan to unblock this is tracked in [#27](https://github.com/workspace-sh/react-native-source-editor/issues/27): port the two Swift files to Obj-C++ so the pod has zero Swift sources, eliminating the modulemap pathway entirely.
9+
```sh
10+
npm run macos:install # install JS deps
11+
npm run macos:pods # pod install (CocoaPods + RN's first-party SPM)
12+
npm run macos:dev # metro + xcodebuild + launch
13+
```
1014

11-
Once that lands, this example will follow react-native-macos's normal idioms (`pod install`, `npm run macos`) — distinct from the iOS example which is owned by Expo CNG and must never be `pod install`-ed manually.
15+
`macos:dev` is `concurrently "macos:clear" "macos:run"` — same shape as the iOS workflow, but driven by RN's bare `react-native run-macos` (no Expo CNG here).
16+
17+
## Toolchain notes
18+
19+
- **react-native-macos 0.81 + Xcode 26 + New Architecture (Fabric)**.
20+
- **STTextView is wired through React Native's first-party `spm_dependency` helper** (lives in `react_native_pods.rb`). The library podspec's SPM declaration is gated on `ENV['RNSE_USE_RN_SPM']`, which this app's Podfile sets — so the third-party `cocoapods-spm` plugin stays out of the chain on macOS. (It has an unfixed Xcode 26 regression, [issue #172](https://github.com/trinhngocthuyen/cocoapods-spm/issues/172), and the RN community is migrating off it.)
21+
- **Static linkage** (RN's macOS default). The SPM helper warns about static linking, but switching to `use_frameworks! :linkage => :dynamic` triggers a separate RN-macOS bug where React-Core's `RCTView.m` hardcodes a class ref to `RCTTextView` that doesn't resolve across dynamic-framework boundaries. Static is fine for our single-product STTextView dep.
22+
- **fmt + Apple Clang 17**: a small post-install patch in `macos/Podfile` scopes `FMT_USE_CONSTEVAL=0` to Apple Clang 17+ (Xcode 26 ships a broken consteval implementation).
23+
24+
## What's distinct from the iOS example
25+
26+
The iOS example (`example/ios-app/`) is owned by Expo CNG — never `pod install` it manually. This macOS example is bare RN macos and uses pod install + xcodebuild directly, the standard react-native-macos workflow.

example/macos-app/macos/MacosApp-macOS/Info.plist

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,18 @@
2323
<key>LSMinimumSystemVersion</key>
2424
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
2525
<key>NSAppTransportSecurity</key>
26-
<dict>
27-
<key>NSAllowsArbitraryLoads</key>
28-
<true/>
29-
<key>NSExceptionDomains</key>
30-
<dict>
31-
<key>localhost</key>
32-
<dict>
33-
<key>NSExceptionAllowsInsecureHTTPLoads</key>
34-
<true/>
35-
</dict>
36-
</dict>
37-
</dict>
26+
<dict>
27+
<key>NSAllowsArbitraryLoads</key>
28+
<true/>
29+
<key>NSExceptionDomains</key>
30+
<dict>
31+
<key>localhost</key>
32+
<dict>
33+
<key>NSExceptionAllowsInsecureHTTPLoads</key>
34+
<true/>
35+
</dict>
36+
</dict>
37+
</dict>
3838
<key>NSMainStoryboardFile</key>
3939
<string>Main</string>
4040
<key>NSPrincipalClass</key>
@@ -43,5 +43,7 @@
4343
<true/>
4444
<key>NSSupportsSuddenTermination</key>
4545
<true/>
46+
<key>RCTNewArchEnabled</key>
47+
<true/>
4648
</dict>
4749
</plist>

0 commit comments

Comments
 (0)