Skip to content

feat(macos): macOS support via RN's first-party SPM helper#30

Merged
LeslieOA merged 1 commit into
developfrom
feat/macos-support
May 8, 2026
Merged

feat(macos): macOS support via RN's first-party SPM helper#30
LeslieOA merged 1 commit into
developfrom
feat/macos-support

Conversation

@LeslieOA
Copy link
Copy Markdown
Member

@LeslieOA LeslieOA commented May 8, 2026

Summary

  • Closes Port Highlighter.swift + SourceEditorImpl.swift to Obj-C++ for macOS support #27 — the macOS example app now builds and runs the editor (markdown, JSON, JavaScript, TypeScript, HTML highlighting all working).
  • The original Port Highlighter.swift + SourceEditorImpl.swift to Obj-C++ for macOS support #27 plan ("port Swift sources to Obj-C++ to escape modulemap issues") was based on a wrong diagnosis. The actual blocker was cocoapods-spm 0.1.20 producing a malformed Pods.xcodeproj on Xcode 26 — a UUID collision between its STTextView `XCSwiftPackageProductDependency` and the `PBXProject` root object, causing Xcode to send `_setSavedArchiveVersion:` to the wrong type and bail. (cocoapods-spm#172 tracks it; no upstream fix.)
  • Switch the macOS path to React Native's first-party `spm_dependency` helper (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. iOS-via-Expo-CNG is untouched and keeps using cocoapods-spm via `app.plugin.js` — that path was never broken.

Key changes

File Why
`ReactNativeSourceEditor.podspec` Add `:osx => '14.0'`. Gate the SPM declaration on `ENV['RNSE_USE_RN_SPM']` so iOS keeps using cocoapods-spm and macOS uses RN's first-party helper. (Can't gate on `s.respond_to?` — 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 the framework-form path (`use_frameworks!` builds a real .framework wrapper); macOS falls back to quote-form which picks the header up from DerivedSources/ (no .framework wrapper under default static-library linkage).
`example/macos-app/macos/Podfile` Drop `plugin 'cocoapods-spm'` and `spm_pkg`; set `ENV['RNSE_USE_RN_SPM'] = '1'`. Stay on static linkage — `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.
`example/macos-app/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.

Plus auto-edits from `pod install` (Pods integration in pbxproj, `RCTNewArchEnabled = true` in Info.plist), `Podfile.lock` and `PrivacyInfo.xcprivacy` checked in for reproducibility, and `example/macos-app/README.md` rewritten to document the now-shipping workflow.

Test plan

  • `npm run macos:clean && npm run macos:pods && npm run macos:dev` builds and launches; editor + preview render with syntax highlighting across all five language tabs.
  • `npm run ios:run` (Expo CNG) — 0 errors, 0 warnings, app installs and launches on simulator (verifies the podspec ENV gate and `__has_include` guard didn't regress iOS).

🤖 Generated with Claude Code

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>
@LeslieOA LeslieOA merged commit 3f3a5a3 into develop May 8, 2026
2 checks passed
@LeslieOA LeslieOA deleted the feat/macos-support branch May 8, 2026 17:06
LeslieOA added a commit that referenced this pull request May 10, 2026
Project hygiene now that #27 is closed:
- README, docs/installation.md, docs/examples.md no longer say macOS is
  pending. Platform support tables, roadmap, and run instructions all
  reflect macOS shipping.
- docs/installation.md: rewritten SPM bridging section. Two paths
  (cocoapods-spm for iOS-via-Expo-CNG, RN's first-party `spm_dependency`
  for bare react-native-macos) with full Podfile snippets, plus the
  static-linkage caveat and the fmt/Apple-Clang-17 patch pointer.
- README's "Repo layout" updated: ios/ is shared across platforms (with
  `#if os(iOS) / #elseif os(macOS)` branching), example/macos-app/ is
  runnable, and `app.plugin.js` is iOS-only.
- README's "Running the macOS example" section replaced with the actual
  npm scripts.

Bump version 0.0.1 → 0.1.0 (still pre-npm; the README explicitly reserves
npm publishing for v1.0). First milestone tag — both target platforms
shipping.

Add CHANGELOG.md following Keep a Changelog 1.1.0. The 0.1.0 entry covers
all merged work to date (PRs #11 through #30).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port Highlighter.swift + SourceEditorImpl.swift to Obj-C++ for macOS support

1 participant