Skip to content

[iOS][swiftpm] Derive an SPM library's Swift name from its podspec - #58290

Open
chrfalch wants to merge 3 commits into
mainfrom
spm/derive-name-from-podspec
Open

[iOS][swiftpm] Derive an SPM library's Swift name from its podspec#58290
chrfalch wants to merge 3 commits into
mainfrom
spm/derive-name-from-podspec

Conversation

@chrfalch

@chrfalch chrfalch commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Summary:

An autolinked library's SwiftPM target name is also its header import prefix, so deriving it from the npm package name was wrong for most of the ecosystem (react-native-svg publishes RNSVG, not ReactNativeSvg) and wrong silently — no error, just headers nobody can import under the expected name.

How:

This PR reads the podspec file on scaffolding, and will use the name from the podspec if available. In addition it deprecates the react-native.config.js spm section in favor of the library's package.json file.

Configuration

A package's SwiftPM settings now live in swiftpmConfig in its package.json, following codegenConfig's conventions: name, dependencies, autolinkingPlugin and scaffold for a library, modules and denyPlugins for an app. The spm block in react-native.config.js is deprecated — still read, so nothing breaks, but it warns once per file and package.json wins field by field.

Resolving

A name resolves from swiftpmConfig.name, then the deprecated spm.name, then the podspec's header_dir or name, then the npm name. The podspec is thereby transitional rather than permanent: spm scaffold records the name it derived as swiftpmConfig.name in the library's package.json, so the next run needs no podspec to name it. It never overwrites a name the library already declares, never records a name guessed from the npm name, and reports what it did.

Failsafety

A prefix Swift cannot spell is normalized to the identifier SwiftPM would compile it as, with a warning. A reserved name or two deps landing on one name is a hard error naming swiftpmConfig.name as the fix; the scope-borrowing that auto-corrected collisions is removed, since a name the build invents is a name no #import can predict. Collision checks key on SwiftPM's c99 name, so react-native-svg and react_native_svg no longer pass and then compile as one module.

Implementation

Name resolution reads the two podspec fields it needs with the regex parser, so it adds no pod ipc spec spawn, and the full read is memoized on the resolved path so one run reads a podspec once across name resolution, header search paths and scaffolding.

rn-tester and the Apple test library move to the new location.

Changelog:

[IOS] [FIXED] - Read SwiftPM name from podspec and store in package.json when scaffolding

Test Plan:

✅ Unit tests

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 2, 2026
@facebook-github-tools facebook-github-tools Bot added p: Expo Partner: Expo Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Sep 2, 2026
@chrfalch
chrfalch force-pushed the spm/derive-name-from-podspec branch 3 times, most recently from c66bafe to 9dda3a4 Compare September 2, 2026 14:18
@chrfalch
chrfalch requested a review from cipolleschi September 2, 2026 14:44

@cipolleschi cipolleschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for putting this together—the direction addresses a real gap. I’m requesting changes for the two concrete blockers called out inline: the current RNTester SwiftPM builds fail after the target-name change, and the fast podspec parsing can promote a subspec header_dir to the package name for external libraries. Once those are fixed with regression coverage, I’ll be happy to take another look.

Comment thread packages/react-native/scripts/spm/__docs__/spm-scripts.md Outdated
Comment thread packages/react-native/scripts/spm/expand-spm-dependencies.js
Comment thread packages/react-native/scripts/spm/read-podspec.js
… config a home

An autolinked library's SwiftPM target name is also its header import prefix,
so deriving it from the npm package name was wrong for most of the ecosystem
(react-native-svg publishes RNSVG, not ReactNativeSvg) and wrong silently — no
error, just headers nobody can import under the expected name.

A package's SwiftPM settings now live in `swiftpmConfig` in its package.json,
following codegenConfig's conventions: `name`, `dependencies`,
`autolinkingPlugin` and `scaffold` for a library, `modules` and `denyPlugins`
for an app. The `spm` block in react-native.config.js is deprecated — still
read, so nothing breaks, but it warns once per file and package.json wins field
by field.

A name resolves from `swiftpmConfig.name`, then the deprecated `spm.name`, then
the podspec's `header_dir` or name, then the npm name. The podspec is thereby
transitional rather than permanent: `spm scaffold` records the name it derived
as `swiftpmConfig.name` in the library's package.json, so the next run needs no
podspec to name it. It never overwrites a name the library already declares,
never records a name guessed from the npm name, and reports what it did.

A prefix Swift cannot spell is normalized to the identifier SwiftPM would
compile it as, with a warning. A reserved name or two deps landing on one name
is a hard error naming swiftpmConfig.name as the fix; the scope-borrowing that
auto-corrected collisions is removed, since a name the build invents is a name
no #import can predict. Collision checks key on SwiftPM's c99 name, so
react-native-svg and react_native_svg no longer pass and then compile as one
module.

Name resolution reads the two podspec fields it needs with the regex parser, so
it adds no `pod ipc spec` spawn, and the full read is memoized on the resolved
path so one run reads a podspec once across name resolution, header search paths
and scaffolding.

rn-tester and the Apple test library move to the new location.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chrfalch
chrfalch force-pushed the spm/derive-name-from-podspec branch from 9dda3a4 to e19cd06 Compare September 2, 2026 16:15
A SwiftPM target name is also the prefix its consumers import it under, so
deriving it from the podspec has to match what CocoaPods already named the
module. CocoaPods resolves `module_name || c99(header_dir) || c99(name)`
(cocoapods-core specification.rb); this consulted only `header_dir` and the
pod name.

`react-native-maps` declares `s.name = "react-native-maps"` and
`s.module_name = "ReactNativeMaps"` with no `header_dir`, so it resolved to
`react-native-maps` — compiled as the module `react_native_maps` — breaking
every `import ReactNativeMaps` and `#import <ReactNativeMaps/...>`. The npm
transform it replaced happened to agree with CocoaPods here.

`header_dir` still wins, being the prefix a library sets when it differs from
its pod name; `module_name` comes next; the pod name is last. The regex fast
path learns `module_name` too, and declines when one is declared but not
literal — without both, the fast path answers first and the new tier never
runs.

Also:

- Both in-repo fixtures declare `swiftpmConfig.name`, so `spm scaffold` no
  longer writes into tracked files on every run, in CI and locally.
- A podspec that yields no name warns, naming `swiftpmConfig.name`: the
  fallback depends on whether CocoaPods is installed, so the same library
  could otherwise be named differently on two machines.
- Two errors named the deprecated config block; they now describe the
  dependency without naming a location, which is correct whichever the
  library uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chrfalch

chrfalch commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Thank you for putting this together—the direction addresses a real gap. I’m requesting changes for the two concrete blockers called out inline: the current RNTester SwiftPM builds fail after the target-name change, and the fast podspec parsing can promote a subspec header_dir to the package name for external libraries. Once those are fixed with regression coverage, I’ll be happy to take another look.

Fixed - Good catch.

The fast path is now anchored to the root spec. A subspec's ss.header_dir can no longer name the library. s.name = 'RNSVG' with a nested ss.header_dir = 'rnsvg' resolves RNSVG. react-native-screens behaves the same.

If a value is declared but not readable, the fast path declines. pod ipc spec then resolves it.

Tests: "does not take a subspec's header_dir as the library's" and "keeps the spec's own module_name, not a subspec's".

@chrfalch

chrfalch commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Both blockers are fixed. A later review found a third.

Podspec module_name was ignored. react-native-maps sets s.name = "react-native-maps" and s.module_name = "ReactNativeMaps", with no header_dir. So it resolved to react-native-maps, and the module became react_native_maps. Every import ReactNativeMaps broke.

The order is now header_dir → module_name → pod name. This matches CocoaPods: module_name || c99(header_dir) || c99(name).

No CI job would have caught it. RNTester has only the two fixtures.

Two smaller fixes. Both fixtures declare swiftpmConfig.name, so spm scaffold no longer writes to tracked files. A podspec with no readable name now warns, because the fallback depends on CocoaPods being installed.

929 tests, 20 suites.

@chrfalch
chrfalch requested a review from cipolleschi September 2, 2026 16:58

@cipolleschi cipolleschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for addressing the earlier feedback and restoring the SwiftPM end-to-end builds. I left two follow-up questions about the intended one-target treatment of subspecs and how that model should distinguish a module name from its header namespace.

// The same, but only where the receiver starts the statement — so a subspec's
// block variable (`ss.header_dir`, the shape react-native-screens and
// react-native-svg ship) cannot answer for the spec itself.
function getSpecStringField(name /*: string */) /*: string | null */ {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks—the ss fix and regression coverage make the intended parent naming much clearer. One question about the flattening model: are all podspec subspecs intended to become one SwiftPM target named after the parent spec? If so, should identity fields from nested scopes always be ignored, independently of the block-variable spelling? getSpecStringField would still accept a nested s.header_dir when a subspec reuses do |s|, for example. Would it be worth covering or documenting that shape so a subspec cannot accidentally rename the parent target?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — it was a bug, not a doc gap. A subspec spelled do |s| did rename the parent (header_dir resolved to the child value); my earlier fix anchored on the receiver spelling, not on scope.

To your question: yes — all subspecs become one target named after the parent, and identity fields from a nested scope should always be ignored, whatever the block variable is called. A regex over flat text cannot see block scope, so the fast path now declines when a subspec re-binds s or spec, and pod ipc spec answers instead. The same guard covers the pure-JS path, otherwise the bug survives wherever CocoaPods is absent. Merge-intended fields (source globs, header mapping dirs, dependencies, search paths) are unchanged.

// declared `header_dir` IS the prefix a library's consumers write, so it keeps
// winning. `module_name` comes next, being what Swift and `@import` consumers
// spell, and the pod name — often dashed — is the last resort.
for (const candidate of [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related question: how should we represent a pod whose module_name and header_dir intentionally differ? CocoaPods keeps those as separate concepts, while this single resolved name picks header_dir and never reaches module_name. One external example is react-native-bare-kit, which declares header_dir = "BareKit" and module_name = "react_native_bare_kit". If the parent becomes one SwiftPM target, should its target/module identity follow module_name while header_dir is handled as a header alias/search path, or should we explicitly reject this shape? I mainly want to ensure scaffolding does not persist one namespace as swiftpmConfig.name while silently losing the other.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed on your example: react-native-bare-kit declares header_dir = "BareKit" and module_name = "react_native_bare_kit". We resolve BareKit, while CocoaPods compiles the module as react_native_bare_kit.

One SwiftPM target cannot carry both, since its module name and header namespace are the same string. So header_dir wins deliberately — it is the prefix existing ObjC consumers write.

Rather than change the precedence, spm scaffold now prints the name it chose and where it came from, and when both keys are declared and differ, which value is not used and that swiftpmConfig.name overrides it. So nothing is dropped silently before the name is persisted, which was your main concern.

Treating header_dir as a header alias alongside a module_name identity is the fuller answer — I would rather do that separately than grow this PR. SwiftPM is Preview, so the contract stays cheap to change.

…ld chose

The identity regexes anchor on the receiver spelling (`s.` / `spec.` at the
start of a statement), not on Ruby block scope, so a subspec that re-binds the
parent's own variable answered for the parent:

    s.name = "ParentPod"
    s.subspec "common" do |s|      # reuses |s|, not |ss|
      s.header_dir = "child_prefix"
    end

resolved `child_prefix` as the library's SwiftPM name. A regex over flat text
cannot see block scope, so the fast path now declines when a subspec re-binds
`s` or `spec`, and `pod ipc spec` — which evaluates real Ruby — answers
instead. The pure-JS path reports no identity for that shape either, or the
same podspec would still rename the parent wherever CocoaPods is absent.
Fields subspecs are meant to contribute are untouched: source globs, header
mapping dirs, dependencies and search paths still merge.

A podspec can also declare a `header_dir` and a `module_name` that differ —
`react-native-bare-kit` publishes `BareKit` and `react_native_bare_kit`. One
SwiftPM target cannot carry both, its module name and header namespace being
the same string, and `header_dir` wins as the prefix existing consumers write.
So `spm scaffold` now says which name it chose and where it came from, and
when both keys are declared and differ, which value is not used and that
`swiftpmConfig.name` overrides the choice — the name is written into the
library's package.json, so it should not be picked silently.

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

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Expo Partner: Expo Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants