7979 * one-line redirect shim (`#import <owner>`). Shims that are namespace-
8080 * module members are fine: they import the owning module, so declarations
8181 * stay single-owned.
82+ * R12. A namespace module's OWN umbrella (ReactCommon/<ns...>/React/<Name>.h,
83+ * natural path React/<Name>.h) ships in ReactNativeHeaders — it is NOT
84+ * hoisted into React.framework by R1. ReactNativeHeaders is the LOWER
85+ * layer (React.framework imports it: RCTCallInvoker.h pulls
86+ * <ReactCommon/CallInvoker.h>), so a framework-owned umbrella turns every
87+ * `#include <React/X.h>` inside a lowercase-namespace header into an import
88+ * of module React and closes a cycle:
89+ * React -> ReactNativeHeaders_react -> React
90+ * Found empirically: react/timing/primitives.h -> <React/Debug.h>. Kept in
91+ * ReactNativeHeaders the same spelling still resolves — framework lookup
92+ * misses and clang falls back to the header search path — textually and
93+ * within one artifact, so no module edge is created. These umbrellas are
94+ * objc-blocked by construction (they re-export their module's C++
95+ * surface), so they were never R4 umbrella or R5 module members anyway.
8296 */
8397
8498const fs = require ( 'node:fs' ) ;
@@ -280,6 +294,13 @@ function renderNamespaceUmbrella(
280294 return `#ifdef __OBJC__\n#import <UIKit/UIKit.h>\n#endif\n\n${ imports } \n` ;
281295}
282296
297+ // R12: a namespace module's own umbrella — physically nested inside the module
298+ // it re-exports (ReactCommon/react/debug/React/Debug.h), which is what tells it
299+ // apart from the ~310 genuine React.framework headers that also carry a
300+ // `React/` natural path but live under React/, Libraries/, ReactApple/, ...
301+ const NS_MODULE_UMBRELLA_RE /*: RegExp */ =
302+ / ^ R e a c t C o m m o n \/ .+ \/ R e a c t \/ [ ^ / ] + \. h $ / ;
303+
283304/**
284305 * Computes the full layout plan from the header inventory manifest
285306 * (build/header-inventory.json — regenerate with header-inventory.js).
@@ -307,7 +328,13 @@ function planFromInventory(
307328 let bucketKey ;
308329 let entryList ;
309330 let relPath ;
310- if ( np . startsWith ( 'React/' ) ) {
331+ if ( np . startsWith ( 'React/' ) && NS_MODULE_UMBRELLA_RE . test ( source ) ) {
332+ // R12: a namespace module's own umbrella stays in the LOWER layer, so
333+ // including it from that namespace cannot create a React module edge.
334+ relPath = np ;
335+ bucketKey = `ReactNativeHeaders/${ relPath } ` ;
336+ entryList = reactNativeHeaders ;
337+ } else if ( np . startsWith ( 'React/' ) ) {
311338 relPath = np . slice ( 6 ) ; // R1: hoist React/ to the framework Headers root
312339 bucketKey = `React.framework/${ relPath } ` ;
313340 entryList = react ;
@@ -332,8 +359,13 @@ function planFromInventory(
332359 seen . set ( bucketKey , source ) ;
333360 entryList . push ( { relPath, source, naturalPath : np } ) ;
334361
335- // R4: React umbrella membership.
336- if ( np . startsWith ( 'React/' ) && isUmbrellaSafe ( h , root ) ) {
362+ // R4: React umbrella membership. Only headers the framework actually ships
363+ // (R12 umbrellas carry a React/ natural path but live in ReactNativeHeaders).
364+ if (
365+ entryList === react &&
366+ np . startsWith ( 'React/' ) &&
367+ isUmbrellaSafe ( h , root )
368+ ) {
337369 umbrella . push ( np ) ;
338370 }
339371 // R5: namespace modules (only for ReactNativeHeaders namespaces). Every
@@ -346,6 +378,19 @@ function planFromInventory(
346378 if ( entryList === reactNativeHeaders ) {
347379 const ns = np . split ( '/' ) [ 0 ] ;
348380 if ( isUmbrellaSafe ( h , root ) ) {
381+ // R12 assert: an R12-routed umbrella lands under the `React` namespace
382+ // here, and renderNamespaceModuleMap only renames the lowercase `react`
383+ // one — so its module would be named `React` and alias the framework
384+ // module, reintroducing the cycle R12 exists to break. These umbrellas
385+ // are objc-blocked today so they never reach this branch; fail closed
386+ // if that ever changes.
387+ if ( ns === 'React' ) {
388+ throw new Error (
389+ `R12: '${ np } ' is a modular candidate in ReactNativeHeaders. Its ` +
390+ `namespace module would be named 'React' and alias the React ` +
391+ `framework module. Keep it out of the modular surface.` ,
392+ ) ;
393+ }
349394 // R5 exemption assert: a namespace whose name is not a valid module
350395 // identifier cannot get a module, so a modular-candidate header in it
351396 // would be silently non-modular — consumers importing it from a
0 commit comments