[iOS][swiftpm] Derive an SPM library's Swift name from its podspec - #58290
[iOS][swiftpm] Derive an SPM library's Swift name from its podspec#58290chrfalch wants to merge 3 commits into
Conversation
c66bafe to
9dda3a4
Compare
cipolleschi
left a comment
There was a problem hiding this comment.
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.
… 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>
9dda3a4 to
e19cd06
Compare
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>
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". |
|
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. |
cipolleschi
left a comment
There was a problem hiding this comment.
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 */ { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 [ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
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-svgpublishesRNSVG, notReactNativeSvg) 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
spmsection in favor of the library'spackage.jsonfile.Configuration
A package's SwiftPM settings now live in
swiftpmConfigin itspackage.json, followingcodegenConfig's conventions:name,dependencies,autolinkingPluginandscaffoldfor a library,modulesanddenyPluginsfor an app. Thespmblock 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 deprecatedspm.name, then the podspec'sheader_diror name, then the npm name. The podspec is thereby transitional rather than permanent:spm scaffoldrecords the name it derived asswiftpmConfig.namein 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 specspawn, 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-testerand 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