4747 * non-modular framework headers; yoga + RCTDeprecation found
4848 * empirically). Namespaces whose name is not a valid module identifier
4949 * (e.g. jsinspector-modern) are exempt — they have no candidates today;
50- * planFromInventory FAILS CLOSED if that changes. `react/` is also exempt: its few
51- * objc-modular-candidates stay textual (as they already were inside
52- * React.framework) so no `react` module aliases the `React` framework
53- * module.
50+ * planFromInventory FAILS CLOSED if that changes. `react/` gets one
51+ * too, named ReactNativeHeaders_react so it cannot alias the `React`
52+ * framework module on a case-insensitive filesystem.
5453 * R6. Bare root aliases are servable only as `<React/X>` — bare angle forms
5554 * (`#import <RCTAppDelegate.h>`) have no framework spelling. This is the
5655 * accepted, measured consumer migration (~4 lines ecosystem-wide).
9392 * within one artifact, so no module edge is created. These umbrellas are
9493 * objc-blocked by construction (they re-export their module's C++
9594 * surface), so they were never R4 umbrella or R5 module members anyway.
95+ * R13. The C++ stable API guards (react/cxxstableapi/*Guard.h) are
96+ * `textual header` members of their R5 module, as is every R5 member that
97+ * reaches one through unguarded includes. A guard decides at each
98+ * #include, from the includer's RN_BUILDING / RN_UMBRELLA_CONTEXT, whether
99+ * that include is allowed. A module member is instead compiled once, on
100+ * its own, with the consumer's -D macros and no includer at all, so under
101+ * RN_STRICT_API the guard fires against the module itself and every
102+ * `import React` fails to build. A textual member still belongs to the
103+ * module, so a framework module may include it (R5), but it is expanded
104+ * at each include site, where the guard sees its real includer.
96105 */
97106
98107const fs = require ( 'node:fs' ) ;
@@ -124,6 +133,8 @@ export type HeadersSpecPlan = {
124133 umbrella: Array<string>,
125134 // R5: plain modules for ReactNativeHeaders' module.modulemap
126135 namespaceModules: {[ns: string]: Array<string>},
136+ // R13: the namespaceModules members declared `textual header`.
137+ textualNamespaceHeaders: Array<string>,
127138 // R10: per-namespace umbrella headers emitted into ReactNativeHeaders.
128139 namespaceUmbrellas: Array<{relPath: string, content: string}>,
129140 // R9: private headers added to the React module map (allowlist).
@@ -268,6 +279,39 @@ function isUmbrellaSafe(h /*: any */, rnRoot /*: string */) /*: boolean */ {
268279 }
269280}
270281
282+ // R13: natural-path prefix of the C++ stable API guard headers.
283+ const STABLE_API_GUARD_PREFIX = 'react/cxxstableapi/' ;
284+
285+ // R13: the inventory headers that are a stable API guard or reach one through
286+ // unguarded includes — what compiling the header on its own in Objective-C
287+ // (a namespace module build) evaluates.
288+ function stableApiGuardedHeaders ( manifest /*: any */ ) /*: Set<string> */ {
289+ const includes /*: Map<string, Array<string>> */ = new Map ( ) ;
290+ for ( const h of manifest . headers ) {
291+ includes . set (
292+ h . naturalPath ,
293+ ( h . includes ?. internal ?? [ ] )
294+ . filter ( i => ! i . cxxGuarded )
295+ . map ( i => i . naturalPath ) ,
296+ ) ;
297+ }
298+ const isGuard = ( np /*: string */ ) => np . startsWith ( STABLE_API_GUARD_PREFIX ) ;
299+ const guarded /*: Set<string> */ = new Set (
300+ Array . from ( includes . keys ( ) ) . filter ( isGuard ) ,
301+ ) ;
302+ let changed = true ;
303+ while ( changed ) {
304+ changed = false ;
305+ for ( const [ np , deps ] of includes ) {
306+ if ( ! guarded . has ( np ) && deps . some ( d => isGuard ( d ) || guarded . has ( d ) ) ) {
307+ guarded . add ( np ) ;
308+ changed = true ;
309+ }
310+ }
311+ }
312+ return guarded ;
313+ }
314+
271315// R10: per-namespace umbrella headers. Some consumers (e.g. Expo's
272316// RCTAppDelegateUmbrella.h) probe
273317// `<React_RCTAppDelegate/React_RCTAppDelegate-umbrella.h>` via __has_include.
@@ -319,6 +363,8 @@ function planFromInventory(
319363 const reactNativeHeaders /*: Array<SpecEntry> */ = [ ] ;
320364 const umbrella /*: Array<string> */ = [ ] ;
321365 const namespaceModules /*: {[string]: Array<string>} */ = { } ;
366+ const textualNamespaceHeaders /*: Array<string> */ = [ ] ;
367+ const guardedHeaders = stableApiGuardedHeaders ( manifest ) ;
322368 const collisions /*: Array<string> */ = [ ] ;
323369 const seen /*: Map<string, string> */ = new Map ( ) ;
324370
@@ -409,6 +455,9 @@ function planFromInventory(
409455 namespaceModules [ ns ] = [ ] ;
410456 }
411457 namespaceModules [ ns ] . push ( np ) ;
458+ if ( guardedHeaders . has ( np ) ) {
459+ textualNamespaceHeaders . push ( np ) ;
460+ }
412461 }
413462 }
414463 }
@@ -417,6 +466,7 @@ function planFromInventory(
417466 for ( const ns of Object . keys ( namespaceModules ) ) {
418467 namespaceModules [ ns ] . sort ( ) ;
419468 }
469+ textualNamespaceHeaders . sort ( ) ;
420470
421471 // R11: assign redirect shims for duplicate spellings of one source.
422472 // Owner precedence: the React/ form (module React / canonical textual home)
@@ -486,6 +536,7 @@ function planFromInventory(
486536 depsNamespaces : DEPS_NAMESPACES ,
487537 umbrella,
488538 namespaceModules,
539+ textualNamespaceHeaders,
489540 namespaceUmbrellas,
490541 privateReactHeaders : PRIVATE_REACT_HEADERS ,
491542 collisions,
@@ -525,10 +576,13 @@ function renderUmbrellaHeader(umbrella /*: Array<string> */) /*: string */ {
525576 * modules, one per namespace with modular candidates — discovered implicitly
526577 * by clang via the auto-added header search path. Headers are referenced by
527578 * their path relative to the Headers root (= the modulemap's directory).
579+ * Members in `textualHeaders` (R13) are declared `textual header`.
528580 */
529581function renderNamespaceModuleMap (
530582 namespaceModules /*: {[string]: Array<string>} */ ,
583+ textualHeaders /*: ReadonlyArray<string> */ = [ ] ,
531584) /*: string */ {
585+ const textual = new Set ( textualHeaders ) ;
532586 // The module NAME is internal to clang's module graph (consumers never
533587 // `@import` these; they `#import <ns/...>` and clang maps the header to its
534588 // module). It only has to be unique and must not alias the `React` framework
@@ -539,7 +593,9 @@ function renderNamespaceModuleMap(
539593 ns === 'react' ? 'ReactNativeHeaders_react' : ns ;
540594 const blocks = [ ] ;
541595 for ( const ns of Object . keys ( namespaceModules ) . sort ( ) ) {
542- const headerLines = namespaceModules [ ns ] . map ( hh => ` header "${ hh } "` ) ;
596+ const headerLines = namespaceModules [ ns ] . map ( hh =>
597+ textual . has ( hh ) ? ` textual header "${ hh } "` : ` header "${ hh } "` ,
598+ ) ;
543599 // R10: the per-namespace umbrella is itself a module member, so importing
544600 // it stays modular (otherwise it re-trips -Wnon-modular-include inside the
545601 // consumer's framework module).
0 commit comments